Site icon GetPageSpeed

Magento 2 Varnish Multi-Store VCL for a Single Server

đź“… Updated: February 19, 2026 (Originally published: February 13, 2017)

Running multiple Magento 2 stores on a single server with Varnish Cache requires a properly configured VCL that handles host-based cache separation and purging. This Magento 2 Varnish multi-store guide provides a production-ready VCL configuration for hosting multiple installations or multi-store setups on shared infrastructure.

The Challenge of Magento 2 Varnish Multi-Store Setups

When you host multiple Magento 2 websites on a single server, the default Magento-generated VCL doesn’t handle cache purging correctly. The problem is that purge requests from one store can accidentally clear cache for all stores, leading to:

The solution is to modify the VCL to include host-based cache banning, ensuring each Magento 2 store’s cache remains isolated.

Why the Default Magento VCL Falls Short

Magento 2 generates a VCL file via the admin panel at Stores > Configuration > Advanced > System > Full Page Cache. While this VCL works for single-store deployments, it has a critical flaw for multi-store environments: the purge logic uses only the X-Magento-Tags header without considering which host initiated the request.

When you save a product in Store A, Magento sends a PURGE request with tags like cat_p_123. The default VCL bans all cached objects matching these tags—including identical product IDs on Store B, even though they’re completely different stores.

Complete Magento 2 Varnish Multi-Store VCL

Below is a production-tested Varnish VCL that properly handles multiple Magento 2 installations or multi-store configurations. This configuration is compatible with Varnish Cache 4.x and later.

vcl 4.0;

import std;

# The minimal Varnish version is 4.0
# For SSL offloading, pass the following header in your proxy server or load balancer: 'X-Forwarded-Proto: https'

backend default {
  .host = "localhost";
  .port = "8080";
  .first_byte_timeout = 600s;
  .probe = {
    .url = "/health_check.php";
    .timeout = 2s;
    .interval = 5s;
    .window = 10;
    .threshold = 5;
  }
}

acl purge {
  "localhost";
}

sub vcl_recv {
  if (req.method == "PURGE") {
    if (client.ip !~ purge) {
      return (synth(405, "Method not allowed"));
    }
    if (!req.http.X-Magento-Tags-Pattern) {
      return (purge);
    }
    # Host-based banning for multi-store support
    if (req.http.host && req.http.host != "" && req.http.host != "127.0.0.1") {
      ban("obj.http.X-Host ~ " + req.http.host + " && obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
    } else {
      ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
    }
    return (synth(200, "Purged"));
  }

  if (req.method != "GET" &&
      req.method != "HEAD" &&
      req.method != "PUT" &&
      req.method != "POST" &&
      req.method != "TRACE" &&
      req.method != "OPTIONS" &&
      req.method != "DELETE") {
    return (pipe);
  }

  if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
  }

  if (req.url ~ "/checkout") {
    return (pass);
  }

  set req.url = regsub(req.url, "^http[s]?://", "");
  std.collect(req.http.Cookie);

  # Strip marketing query parameters
  if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") {
    set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
    set req.url = regsub(req.url, "[?|&]+$", "");
  }

  # Remove cookies for static assets
  if (req.url ~ "^/(media|static)/.*\.(ico|css|js|jpg|jpeg|png|gif|tiff|bmp|mp3|ogg|svg|swf|woff|woff2|eot|ttf|otf)$") {
    unset req.http.Https;
    unset req.http.X-Forwarded-Proto;
    unset req.http.Cookie;
  }

  # Pass authenticated GraphQL requests
  if (req.url ~ "/graphql" && req.http.Authorization ~ "^Bearer") {
    return (pass);
  }

  return (hash);
}

sub vcl_hash {
  # Include X-Magento-Vary cookie in cache key
  if (req.http.cookie ~ "X-Magento-Vary=") {
    hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1"));
  }

  # Include host in cache key for multi-store support
  if (req.http.host) {
    hash_data(req.http.host);
  } else {
    hash_data(server.ip);
  }

  if (req.url ~ "/graphql") {
    call process_graphql_headers;
  }

  # Include protocol in cache key (http vs https)
  if (req.http.X-Forwarded-Proto) {
    hash_data(req.http.X-Forwarded-Proto);
  }
}

sub process_graphql_headers {
  if (req.http.Store) {
    hash_data(req.http.Store);
  }
  if (req.http.Content-Currency) {
    hash_data(req.http.Content-Currency);
  }
}

sub vcl_backend_response {
  # Store host for later use in banning
  set beresp.http.X-Host = bereq.http.host;
  set beresp.grace = 3d;

  if (beresp.http.content-type ~ "text") {
    set beresp.do_esi = true;
  }

  if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") {
    set beresp.do_gzip = true;
  }

  if (beresp.status != 200 && beresp.status != 404) {
    set beresp.ttl = 0s;
    set beresp.uncacheable = true;
    return (deliver);
  } elsif (beresp.http.Cache-Control ~ "private") {
    set beresp.uncacheable = true;
    set beresp.ttl = 86400s;
    return (deliver);
  }

  if (beresp.http.X-Magento-Debug) {
    set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
  }

  if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
    unset beresp.http.set-cookie;
    if (bereq.url !~ "\.(ico|css|js|jpg|jpeg|png|gif|tiff|bmp|gz|tgz|bz2|tbz|mp3|ogg|svg|swf|woff|woff2|eot|ttf|otf)(\?|$)") {
      set beresp.http.Pragma = "no-cache";
      set beresp.http.Expires = "-1";
      set beresp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
      set beresp.grace = 1m;
    }
  }

  # Cache search results for 30 minutes
  if (bereq.url ~ "/catalogsearch") {
    set beresp.ttl = 30m;
  }

  if (beresp.ttl <= 0s ||
      beresp.http.Surrogate-control ~ "no-store" ||
      (!beresp.http.Surrogate-Control && beresp.http.Vary == "*")) {
    set beresp.ttl = 120s;
    set beresp.uncacheable = true;
  }

  return (deliver);
}

sub vcl_deliver {
  if (resp.http.X-Magento-Debug) {
    if (resp.http.x-varnish ~ " ") {
      set resp.http.X-Magento-Cache-Debug = "HIT";
    } else {
      set resp.http.X-Magento-Cache-Debug = "MISS";
    }
  } else {
    unset resp.http.Age;
  }

  # Remove internal headers from response
  unset resp.http.X-Magento-Debug;
  unset resp.http.X-Magento-Tags;
  unset resp.http.X-Powered-By;
  unset resp.http.Server;
  unset resp.http.X-Varnish;
  unset resp.http.Via;
  unset resp.http.Link;
}

sub vcl_hit {
  if (obj.ttl >= 0s) {
    return (deliver);
  }

  if (std.healthy(req.backend_hint)) {
    if (obj.ttl + 259200s > 0s) {
      set req.http.grace = "normal (healthy server)";
      return (deliver);
    } else {
      return (fetch);
    }
  } else {
    set req.http.grace = "unlimited (unhealthy server)";
    return (deliver);
  }
}

Key Features for Magento 2 Varnish Multi-Store Environments

Host-Based Cache Banning

The critical difference from the default Magento VCL is in the vcl_recv purge handling:

if (req.http.host && req.http.host != "" && req.http.host != "127.0.0.1") {
  ban("obj.http.X-Host ~ " + req.http.host + " && obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
}

This ensures that when store A purges its product cache, store B’s cache remains untouched. The X-Host header is stored during backend response and matched during ban operations.

Cache Key Isolation

The vcl_hash subroutine includes the host in the cache key:

if (req.http.host) {
  hash_data(req.http.host);
}

This guarantees that https://store-a.com/product` andhttps://store-b.com/product` are cached as separate entries, even if they have identical URLs.

Health Check Configuration

The backend probe checks /health_check.php to ensure Magento is responsive before serving cached content:

.probe = {
  .url = "/health_check.php";
  .timeout = 2s;
  .interval = 5s;
  .window = 10;
  .threshold = 5;
}

Make sure this file exists in your Magento root directory. Magento 2 includes this file by default.

Grace Period for High Availability

The VCL includes a 3-day grace period (beresp.grace = 3d). This means Varnish can serve stale content while fetching fresh content in the background, or when the backend is temporarily unavailable. This is crucial for maintaining uptime during deployments or brief outages.

Marketing Parameter Stripping

The configuration automatically strips common marketing parameters (UTM tags, Facebook click IDs, Google click IDs) from URLs before caching. This prevents cache fragmentation where the same page gets cached multiple times with different tracking parameters.

Configuration Steps

Follow these steps to deploy the Magento 2 Varnish multi-store VCL:

  1. Back up your existing VCL before making changes
  2. Replace your VCL file (typically /etc/varnish/default.vcl) with the configuration above
  3. Adjust the backend port if your NGINX or Apache listens on a different port than 8080
  4. Add your server IPs to the acl purge block if purge requests come from multiple servers
  5. Verify the syntax: varnishd -C -f /etc/varnish/default.vcl
  6. Restart Varnish to apply changes: systemctl restart varnish
  7. Test purging by saving a product in each store and verifying only that store’s cache is cleared

Verifying the Configuration

After deploying the VCL, verify that multi-store isolation works correctly:

# Check cache status for store A
curl -I -H "Host: store-a.com" http://localhost:6081/

# Check cache status for store B  
curl -I -H "Host: store-b.com" http://localhost:6081/

# Monitor ban list
varnishadm ban.list

The X-Magento-Cache-Debug header (when debug mode is enabled) shows whether responses are cache HITs or MISSes.

Troubleshooting Common Issues

Cache not being purged: Ensure the purge ACL includes all IPs that send PURGE requests. Check that Magento’s cache backend is configured to use Varnish.

All stores purged together: Verify the X-Host header is being set correctly in vcl_backend_response. Use varnishlog to inspect headers.

Low hit ratio: Check that cookies are being handled correctly. The X-Magento-Vary cookie should be the only one affecting cache keys for anonymous users.

For a complete Magento 2 + Varnish setup, see these guides:

Conclusion

This Magento 2 Varnish multi-store VCL configuration solves the common problem of cache interference when running multiple stores on a single Varnish instance. By including host-based banning and cache key isolation, each store operates independently while sharing the same Varnish infrastructure. The result is higher cache hit ratios, faster page loads, and more predictable performance across all your Magento 2 stores.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version