Server Setup

Full page cache of Woocommerce product pages for different user roles

by , , revisited on

We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Full page caching can be a useful technique for improving the performance of your WooCommerce website, but it can be challenging to implement it in a way that works for different user roles. This is because different user roles may have different permissions and access to different content, and the cached version of a page may not accurately reflect the content that is visible to a particular user.

To cache WooCommerce product pages with Varnish, you will need to install and configure Varnish on your server and configure it to cache your WooCommerce product pages. This typically involves creating a Varnish configuration file that defines the rules for caching your pages.

Woocommerce product pages can be fully cached by Varnish with the following config:

   if (req.method == "GET" && req.url ~ "^/(shop|product)" && req.url !~ "\?add-to-cart=") {
       unset req.http.cookie;

However, when using plugins, such as woocommerce-wholesale-pricing, a different price must be shown to different WordPress user roles.

To have a full page in such a case, we need to vary page cache based on the logged-in user’s roles.

This requires adding some WordPress code to emit a cookie with user’s roles, as well as Varnish configuration changes, that create cache variations.

WordPress code changes

Our PHP code is pretty simple and can be added to your theme’s functions.php:

add_action('wp_login', function ( $user_login, $user ) {
    if ( ! headers_sent() ) {
        $roles = implode(',', $user->roles);
        setcookie( 'user_roles', $roles, time() + (30 * DAY_IN_SECONDS), COOKIE_PATH, COOKIE_DOMAIN );
}, 10, 2); 

add_action('set_logged_in_cookie', function ( $logged_in_cookie, $expire, $expiration, $user_id ) {
    $user = get_user_by( 'id', $user_id );
    $roles = implode( ',', $user->roles );
    setcookie( 'user_roles', $roles, $expire, COOKIEPATH, COOKIE_DOMAIN, true, true );
}, 10, 4);

We also need to clear the user roles cookie upon logout:

add_action( 'wp_logout', function ( $user_id ) {
    if ( ! headers_sent() ) {
        setcookie( 'user_roles', time() - (30 * DAY_IN_SECONDS) );
} );

It plugs into wp_login hook which happens right after successful user authentication. It then takes the user’s roles, and instructs the browser to create user_roles cookie.
The value of the cookie is a comma-delimited list of the user’s roles, e.g. vat, or administrator.

Now, any time after authentication the user browses the website, its browser sends the user_roles cookie and we can teach Varnish to vary cache based on its value.

Varnish VCL code

The vcl_recv routine

sub vcl_recv {
    if (req.method == "GET" && req.url ~ "^/shop" && req.url !~ "\?add-to-cart=") {
        # set special header for varying cache by roles
        if (req.http.cookie ~ "user_roles=") {
            set req.http.x-user-roles = regsub(req.http.cookie, "^.*?user_roles=([^;]+);*.*$", "\1");
        } else {
            set req.http.x-user-roles = "guest";
        # we must pass through cookies for role-specific content variation 
        # save the cookies before the built-in vcl_recv and restore later for backend to see while caching
        # see:
        set req.http.Cookie-Backup = req.http.Cookie;
        unset req.http.Cookie;

In the sub vcl_recv routine, we specify product pages via regular expression ~/shop. Take note that we should only act on GET requests, because upon POST requests some versions of Woocomerce
have add-to-cart functionality.

If user_roles cookie is present in the Cookie: header, we extract its value and create X-User-Roles header.

Next up, we backup the whole Cookie header in order to be able to cache the request. This is important due to “coding with built-in VCL in mind” pattern.

The vcl_backend_response routine

sub vcl_backend_response {
    # ensure Varnish varies objects by X-User-Roles header value
    # this ensures that upon change, a PURGE will evict all variations
    if (bereq.http.x-user-roles) {
        if (!beresp.http.Vary) { # no Vary at all
            set beresp.http.Vary = "x-user-roles";
        } elseif (beresp.http.Vary !~ "x-user-roles") { # add to existing Vary
            set beresp.http.Vary = beresp.http.Vary + ", x-user-roles";

    # make sure that shop pages are cacheable no matter if we accessed with a session
    # or a bad plugin that emits Set-Cookie for no good reason
    if (bereq.method == "GET" && bereq.url ~ "^/shop" && bereq.url !~ "\?add-to-cart=") {
        unset beresp.http.Set-Cookie;
        unset beresp.http.Cache-Control;
        # while doing Set-Cookie, PHP sends anti-caching header in Cache-Control, so we must explicitly set Varnish TTL to keep caching in Varnish
        set beresp.ttl = 2w;

In vcl_backend_response, the first block of code ensures that Varnish treats different values of the X-User-Roles header as variations of the same object.
This is crucial to allow us easily PURGE all variations of the same product page, that look differently (e.g. has different prices) for each WordPress role.

In the next block of VCL, we ensure the cacheability of the product page even when we’re caching it for different user roles. (note that in our case, Varnish sees request with Cookie: but still does caching).

The to vcl_hash routine

sub vcl_hash {
    if (req.http.Cookie-Backup) {
        # restore the cookies before the lookup if any
        set req.http.Cookie = req.http.Cookie-Backup;
        unset req.http.Cookie-Backup;

Our vcl_hash routine restores the backed up Cookie: header so that the backend can see it and generate appropriate pages with the correct prices for each user role.

The vcl_deliver routine

sub vcl_deliver {
    set resp.http.x-user-roles = req.http.x-user-roles;        

Finally, in the vcl_deliver routine, we emit X-User-Roles header, for debugging and making sure our value extraction from user_roles cookie is correct.

Cache invalidation

When a product is changed, its cache should be invalidated. We throw in the supplementary plugin to send PURGE request for us.
Thanks to the logic we added to our VCL, those purge request will clear up caches for all variations at once.

The plugin can be installed and configured via WP-CLI:

wp plugin install varnish-http-purge --activate --force
wp option add vhp_varnish_ip

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: