WordPress Varnish caching

Your WordPress blog is running very slowly and you have tried 5000 plugins and none of them is helping? This post will demonstrate how to solve the performance issues using the reverse caching proxy.

Varnish Cache is a very common caching tool (reverse caching proxy) many web applications/ blogs/CDNs utilize to speed up content delivery, this blog including.

Here – I will post a simple (or maybe not so simple) Varnish Cache configuration example I use for my varnish docker container:

#######################################################################
# WordPress application
#######################################################################
vcl 4.1;

import std;

sub strip_tracking_cookies {
    set req.http.Cookie = regsuball(req.http.Cookie, "(__utm|_ga|_opt)[a-z_]*=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "(__)?hs[a-z_\-]+=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "hubspotutk=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "_hj[a-zA-Z]+=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "(NID|DSID|__gads|GED_PLAYLIST_ACTIVITY|ACLK_DATA|ANID|AID|IDE|TAID|_gcl_[a-z]*|FLC|RUL|PAIDCONTENT|1P_JAR|Conversion|VISITOR_INFO1[a-z_]*)=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "^;\s*", "");
    if (req.http.cookie ~ "^\s*$") {
        unset req.http.cookie;
    }
}
sub just_cache_statics {
    if (req.url ~ "^[^?]*\.(7z|avi|bmp|bz2|br|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
        unset req.http.Cookie;
        set req.http.x-static-file = "true";

        #This might be problematic, some tools might require that.
        #set req.url = regsub(req.url, "\?.*$", "");
        
        return(hash);
    }
}

# Purgers ips
acl purgers {
  "localhost";
  "127.0.0.1";
  "::1";

  #Tailscale ipv6 ULA
  #https://tailscale.com/kb/1033/ip-and-dns-addresses/
  "fd7a:115c:a1e0:ab12::"/64;

  #https://tailscale.com/kb/1015/100.x-addresses/
  #“Carrier Grade NAT” (CGNAT) address space
  "100.64.0.0"/10;

  #My docker bridge range
  "172.16.0.0"/12;
}
sub vcl_recv {
	unset req.http.x-vcache;
}

sub vcl_hit {
	set req.http.x-vcache = "hit";
	if (obj.ttl <= 0s && obj.grace > 0s) {
		set req.http.x-vcache = "hit graced";
	}
}

sub vcl_miss {
	set req.http.x-vcache = "miss";
}

sub vcl_pass {
	set req.http.x-vcache = "pass";
}

sub vcl_pipe {
	set req.http.x-vcache = "pipe uncacheable";
}

sub vcl_synth {
	set req.http.x-vcache = "synth synth";
	# uncomment the following line to show the information in the response
	set resp.http.x-vcache = req.http.x-vcache;
}

sub vcl_deliver {
	if (obj.uncacheable) {
		set req.http.x-vcache = req.http.x-vcache + " uncacheable" ;
	} else {
		set req.http.x-vcache = req.http.x-vcache + " cached" ;
	}
	# uncomment the following line to show the information in the response
	set resp.http.x-vcache = req.http.x-vcache;
}
sub vcl_recv {
    # Copied from built-ins https://www.varnish-software.com/developers/tutorials/varnish-builtin-vcl/
    #============================================================================
    if (req.method == "PRI") {
        /* This will never happen in properly formed traffic (see: RFC7540) */
        return (synth(405));
    }
    if (!req.http.host &&
      req.esi_level == 0 &&
      req.proto ~ "^(?i)HTTP/1.1") {
        /* In HTTP/1.1, Host is required. */
        return (synth(400));
    }

    # httpoxy mitigation
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#4-httpoxy-mitigation
    unset req.http.proxy;

    # Sorting query string parameters#
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#5-sorting-query-string-parameters   
    set req.url = std.querysort(req.url);

    # Stripping off a trailing question mark#
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#6-stripping-off-a-trailing-question-mark
    set req.url = regsub(req.url, "\?$", "");

    # Strip ampersand right after the question mark, or empty ? sign
    set req.url = regsub(req.url, "\?&", "?");
    set req.url = regsub(req.url, "\?$", "");

    # Dealing with websockets
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#9-dealing-with-websockets
    if (req.http.Upgrade ~ "(?i)websocket") {
        return (pipe);
    }

    # Grace mode (for when the backend is up)
    #https://www.varnish-software.com/developers/tutorials/example-vcl-template/#15-setting-grace-mode
    if (std.healthy(req.backend_hint)) {
        set req.grace = 10s;
    }

    # 7. Removing Google Analytics URL parameters
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#7-removing-google-analytics-url-parameters
    if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|utm_content|utm_term|gclid|cx|ie|cof|siteurl)=") {
        set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_content|utm_term|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "");
        set req.url = regsuball(req.url, "\?(utm_source|utm_medium|utm_campaign|utm_content|utm_term|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "?");
        set req.url = regsub(req.url, "\?&", "?");
        set req.url = regsub(req.url, "\?$", "");
    }
}
sub vcl_recv {
    # Purge logic to remove objects from the cache. 
    # Tailored to the Proxy Cache Purge WordPress plugin
    # See https://wordpress.org/plugins/varnish-http-purge/
    if (req.method == "PURGE") {
        if(!client.ip ~ purgers) {
            return(synth(405,"PURGE not allowed for this IP address"));
        }
        if (req.http.X-Purge-Method == "regex") {
            ban("obj.http.x-url ~ " + req.url);
            return(synth(200, "Purged"));
        }
        ban("obj.http.x-url == " + req.url);
        return(synth(200, "Purged"));
    }
}

sub vcl_backend_response {
    set beresp.http.x-url = bereq.url;
}

sub vcl_deliver {
    unset resp.http.x-url;
}

backend default {
    .host = "production-bytepursuits-nginx";
    .port = "80";
    .max_connections = 10000;
    .connect_timeout        = 5s;
    .first_byte_timeout     = 90s;
    .between_bytes_timeout  = 2s;
}


#######################################################################
# Client side
#######################################################################
# Called at the beginning of a request, after the complete request has been received and parsed.
# Its purpose is to decide whether or not to serve the request, how to do it, and, if applicable,
# which backend to use. also used to modify the request
sub vcl_recv {
    # 2. Piping over non http
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#10-piping-other-non-http-content
    #============================================================================
    if (
        req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "PATCH" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "DELETE" &&
        req.method != "PATCH"
    ) {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }

    # 3. Only cache GET and HEAD
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#11-only-cache-get-and-head-requests
    if (req.method != "GET" && req.method != "HEAD") {
        set req.http.x-cacheable = "no:request-method";
        return (pass);
    }
    
    # 4. Caching static content: Do no pass cookies to static resources: 
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#13-caching-static-content
    # @fixme: this is questionable, we may want to just serve statics from nginx
    call just_cache_statics;

    # 5. Remove common tracking cookies 
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#12-remove-tracking-cookies
    call strip_tracking_cookies;



    # 6. Application specific: WordPress specific skips
    if (
        req.http.Cookie ~ "wordpress_(?!test_)[a-zA-Z0-9_]+|wp-postpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+|wordpress_logged_in_|comment_author|PHPSESSID" ||
        req.http.Authorization ||
        req.url ~ "add_to_cart" ||
        req.url ~ "edd_action" ||
        req.url ~ "nocache" ||
        req.url ~ "^/addons" ||
        req.url ~ "^/bb-admin" ||
        req.url ~ "^/bb-login.php" ||
        req.url ~ "^/bb-reset-password.php" ||
        req.url ~ "^/cart" ||
        req.url ~ "^/checkout" ||
        req.url ~ "^/control.php" ||
        req.url ~ "^/login" ||
        req.url ~ "^/logout" ||
        req.url ~ "^/lost-password" ||
        req.url ~ "^/my-account" ||
        req.url ~ "^/product" ||
        req.url ~ "^/register" ||
        req.url ~ "^/register.php" ||
        req.url ~ "^/server-status" ||
        req.url ~ "^/signin" ||
        req.url ~ "^/signup" ||
        req.url ~ "^/stats" ||
        req.url ~ "^/wc-api" ||
        req.url ~ "^/wp-admin" ||
        req.url ~ "^/wp-comments-post.php" ||
        req.url ~ "^/wp-cron.php" ||
        req.url ~ "^/wp-login.php" ||
        req.url ~ "^/wp-activate.php" ||
        req.url ~ "^/wp-mail.php" ||
        req.url ~ "^/wp-login.php" ||
        req.url ~ "^\?add-to-cart=" ||
        req.url ~ "^\?wc-api=" ||
        req.url ~ "^/preview=" ||
        req.url ~ "^/\.well-known/acme-challenge/"
    ) {
        set req.http.x-cacheable = "no:logged-in/got-sessions";
        if(req.http.X-Requested-With == "XMLHttpRequest") {
            set req.http.x-cacheable = "no:ajax";
        }
        return(pass);
    }

    # Remove any cookies left (I think for wp specifically this probably makes sense, for other sites not so much)
    unset req.http.Cookie;

    return (hash);
}


# The routine when we deliver the HTTP request to the user
# Last chance to modify headers that are sent to the client
sub vcl_deliver {
    # some includes

    # Debug header: cacheable status
    if(req.http.x-cacheable) {
        set resp.http.x-cacheable = req.http.x-cacheable;
    } elseif(obj.uncacheable) {
        if(!resp.http.x-cacheable) {
        set resp.http.x-cacheable = "no:uncacheable";
        }
    } elseif(!resp.http.x-cacheable) {
        set resp.http.x-cacheable = "yes";
    }

    //Unset some typical headers that just expose information 
    unset resp.http.server;
    unset resp.http.via;
    unset resp.http.x-powered-by;
    unset resp.http.x-runtime;

    # This return is explicit, but wont hurt either way: 
    # https://www.varnish-software.com/developers/tutorials/varnish-builtin-vcl/#8-vcl_deliver
    return (deliver);
}


#######################################################################
# Backend Fetch
#######################################################################
# When your origin server responds to a backend request issued by vcl_backend_fetch
sub vcl_backend_response {
    # some includes

    if (!beresp.http.Cache-Control) {
        set beresp.http.x-cacheable = "no:missing-header";
    }

    # If the file is marked as static we unset cookie and cache it for 1 day.... 
    # https://www.varnish-software.com/developers/tutorials/configuring-varnish-wordpress/
    if (bereq.http.x-static-file == "true") {
        #why would it have cookies? it wouldn't but whatever
        unset beresp.http.Set-Cookie;
        set beresp.http.x-cacheable = "yes:static";
    }
    if (beresp.http.Set-Cookie) {
        set beresp.http.x-cacheable = "no:got cookies";
    } elseif(beresp.http.Cache-Control ~ "private") {
        set beresp.http.x-cacheable = "no:cache-control=private";
    }	


    # Grace mode (for only when the backend is unresponsive)
    # https://www.varnish-software.com/developers/tutorials/example-vcl-template/#15-setting-grace-mode
    set beresp.grace = 6h;

    # Note: that this default VCL will also execute right after
    # https://www.varnish-software.com/developers/tutorials/varnish-builtin-vcl/#11-vcl_backend_response
}


#######################################################################
# Housekeeping
#######################################################################

# Could be used to init VMODs.
sub vcl_init {

}

Where production-bytepursuits-nginx in above example is the hostname of my nginx server.

For the record the wordpress stack flow behind this stack looks like this: haproxy (abused for TLS termination) => Varnish Cache => Nginx => phpfpm (wordpress).

I use the docker-compose, the container name in docker container is usable as a hostname, hence the lack of TLD part.

In fact this very site does use varnish, so you can check if page was served via varnish or not by opening the developer tools and looking at the response headers.

x-vcache = hit cached would then signify that page was served from the Varnish Cache.

Leave a Comment