How to Configure Varnish Soft Purge for Magento 2 (Ubuntu + xkey)

Enable and Configure Soft Purge in Varnish for Magento 2 (Ubuntu + xkey)

When you run Magento 2 behind Varnish on a busy store, cache invalidation can easily become a performance problem.

Traditionally, when a Varnish object (page) gets purged, the next user who hits that page pays the price: Varnish has to fetch from the backend, generate a new response, and only then cache it.

On low-traffic sites this isn’t a huge issue, but on high-traffic stores it becomes painful very quickly:

  • The first user after a purge gets a slow response
  • If 10 users hit the same page while the cache is rebuilding, you get 10 slow responses
  • If you purge often (e.g. frequent product updates), the site feels slow even though Varnish is technically enabled

The solution: Soft Purge using vmod_xkey.

Instead of immediately deleting objects from the cache, Varnish can:

  • mark objects as stale,
  • continue serving them to users,
  • refresh a new version from the backend in the background.

Result: users still see a cached page, while Varnish quietly builds the fresh version.

In this guide we’ll configure Soft Purge for Magento 2 on Ubuntu with Varnish + xkey.

We’ll cover:

  1. Installing varnish-modules (xkey) on Ubuntu
  2. Enabling import xkey; in your VCL
  3. Replacing the default Magento BAN logic with xkey soft purge
  4. Setting grace so Varnish can safely serve stale content
  5. Testing soft purge with curl

Examples assume Magento 2 + Varnish on Ubuntu.


1. Install varnish-modules (xkey) on Ubuntu

On Ubuntu, the vmod_xkey module is provided by the varnish-modules package.

sudo apt update  
sudo apt install varnish-modules  

Check that the xkey vmod is available (path may vary by distro and Varnish version):

ls -1 /usr/lib/varnish/vmods  
# You should see something like: libvmod_xkey.so

Note: if you’re using a custom Docker image or a containerized Varnish setup, vmod_xkey must be built into that image. In some managed environments it’s already included by default.


2. Enable xkey in your VCL

In your main VCL file (often /etc/varnish/default.vcl, or an entry-point VCL that includes the Magento-generated file), add this at the top:

import xkey;  

Example:

vcl 4.1;

import std;  
import xkey;

backend default {  
    .host = "127.0.0.1";
    .port = "8080";
}

If you’re using the Magento-generated VCL (magento2.vcl), make sure import xkey; is added to the actual VCL file loaded by Varnish, not just some unused include.


3. Replace Magento BAN logic with xkey Soft Purge

Magento 2 invalidates cache via HTTP BAN (or PURGE) requests that carry the header X-Magento-Tags-Pattern. In a typical VCL you’ll see something like:

if (req.method == "BAN") {  
    if (req.http.X-Magento-Tags-Pattern) {
        ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
    }
    return (synth(200, "Banned"));
}

With soft purge we keep the idea of tag-based invalidation, but instead of hard-removing objects, we mark them as stale using xkey.softpurge().

In sub vcl_recv, in the PURGE/BAN section, update it to something like this (adapt as needed to match your existing structure):

sub vcl_recv {  
    # ... your existing logic (health checks, static assets, etc.)

    if (req.method == "PURGE") {
        # Full Page Cache flush – still a hard ban
        if (req.http.X-Magento-Tags-Pattern == ".*") {
            ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
        }
        # Soft purge for specific tags
        elseif (req.http.X-Magento-Tags-Pattern) {
            # Example: "((^|,)cat_c(,|$))|((^|,)cat_p(,|$))" → "cat_c cat_p"
            set req.http.X-Magento-Tags-Pattern =
                regsuball(req.http.X-Magento-Tags-Pattern, "[^a-zA-Z0-9_-]+", " ");

            # Trim spaces
            set req.http.X-Magento-Tags-Pattern =
                regsuball(req.http.X-Magento-Tags-Pattern, "(^\\s*)|(\\s*$)", "");

            # Soft purge via xkey
            set req.http.n-gone = xkey.softpurge(req.http.X-Magento-Tags-Pattern);

            return (synth(200, "Invalidated " + req.http.n-gone + " objects"));
        }

        return (synth(200, "Purged"));
    }

    # ... rest of vcl_recv
}

What this does:

  • Takes the original X-Magento-Tags-Pattern (which is usually a regex) and turns it into a space-separated list of tags (cat_c cat_p, cat_p_123, etc.). That’s the format xkey expects.
  • Calls xkey.softpurge() to mark all matching objects as stale instead of fully purged.
  • Returns a synthetic 200 response like Invalidated 5 objects – very handy for debugging with curl.

For a full flush (X-Magento-Tags-Pattern: .*) we still use a classic ban(). In most Magento use cases, that’s exactly what you want when doing a global FPC flush.


4. Configure grace and xkey tags in vcl_backend_response

Soft purge is only useful if Varnish is allowed to serve stale content while it fetches a fresh version in the background. We control that with beresp.grace.

In sub vcl_backend_response, add or update something like this:

sub vcl_backend_response {  
    # Time window during which stale content can be served
    set beresp.grace = 3h;

    # Use xkey based on Magento tags
    if (beresp.http.X-Magento-Tags) {
        # Expose grace for debugging
        set beresp.http.Grace = beresp.grace;

        # Turn comma-separated X-Magento-Tags into a space-separated list for xkey
        set beresp.http.xkey = regsuball(beresp.http.X-Magento-Tags, ",", " ");

        # Optionally reset X-Magento-Tags to a generic value
        # so you don't leak massive tag lists further downstream
        set beresp.http.X-Magento-Tags = "fpc";
    }

    # ... rest of vcl_backend_response
}

Key points:

  • beresp.grace = 3h; tells Varnish: even after TTL expires, you may serve this object as stale for up to 3 hours while you fetch a fresh version.
  • beresp.http.xkey is populated from Magento’s X-Magento-Tags header. That’s what xkey.softpurge() uses to find and mark the right objects as stale.
  • Be careful not to duplicate beresp.grace if your Magento-generated VCL already sets it. Adjust the existing line instead of adding a second one.

5. Reload Varnish with the new VCL

After editing your VCL:

sudo systemctl reload varnish  
# or, if reload is not configured:
sudo systemctl restart varnish  

Confirm that the active VCL is loaded:

sudo varnishadm vcl.list  

6. Testing Soft Purge

6.1. Check tags and xkey headers

First, make a normal GET request to a Magento page (for example, a product page) and inspect the headers:

curl -I https://example.com/some-product.html  

You should see something along the lines of:

  • X-Magento-Tags: cat_p_169745,cat_c_23,...
  • xkey: cat_p_169745 cat_c_23 ...
  • Grace: 3h

Header names can vary slightly depending on your VCL, but the important part is that xkey is populated with Magento’s tag list and Grace is set.

6.2. Soft purging a specific tag

Now trigger a soft purge via PURGE + X-Magento-Tags-Pattern:

curl -k -X PURGE \  
  -H 'X-Magento-Tags-Pattern: cat_p_169745' \
  http://varnish.internal:6081/some-product-url

If everything is wired correctly, you’ll get a synthetic response like:

<!DOCTYPE html>  
<html>  
<head>  
<title>200 Invalidated 1 objects</title>  
</head>  
<body>  
<h1>Error 200 Invalidated 1 objects</h1>  
<p>Invalidated 1 objects</p>  
<h3>Guru Meditation:</h3>  
<p>XID: 65566</p>  
<hr>  
<p>Varnish cache server</p>  
</body>  
</html>  

Important details:

  • HTTP status is 200 (this is not a real error, just Varnish’s synthetic page style).
  • The text says Invalidated 1 objectsxkey.softpurge() has marked 1 object as stale.

6.3. Observing behavior before/after soft purge

Typical flow:

  1. Request before soft purge → HIT, fast response.
  2. After soft purge → first client still gets a fast response (HIT with stale content), while Varnish refreshes the backend in the background.
  3. Subsequent requests → now get the fresh version from the updated backend, still as cache HIT.

If you see MISS and slow responses instead, double-check:

  • Is beresp.grace actually set (and not overridden later)?
  • Does some other part of your VCL do return (pass); for that URL?
  • Is some custom Magento/Varnish module doing extra purges or cache-bypass logic?

7. Notes for Production Environments

A few practical tips before rolling this into production:

  • If you’re using a Magento Varnish extension (e.g. Jetrails Varnish extension or similar):

    • Always upgrade it to the latest version first
    • Regenerate the VCL from Magento
    • Then apply the soft purge modifications on top of that generated VCL
  • If you run a multi-node Varnish cluster:

    • Make sure purge/softpurge is executed on all nodes (via shared admin endpoint, orchestration, or varnishadm over SSH).
  • Be reasonable with grace values:

    • 3h is often a sweet spot for most e‑commerce sites
    • For extremely dynamic content, you might want shorter grace (e.g. 30–60 minutes)
    • You can also tune grace by path (e.g. longer grace for category pages, shorter for carts/checkout which should normally bypass Varnish anyway)

8. References


If you’re debugging a high-traffic Magento 2 store and notice that Varnish is enabled but your users still see slow page loads after product updates, switching from hard purges to soft purges with xkey is one of the highest ROI changes you can make.