Cloudflare SSL + Magento 2 Varnish VCL for Multi-Domain Stores

A practical Magento 2 multi-store Varnish setup behind Cloudflare HTTPS and Apache SSL proxy, using a multi-domain default.vcl that strips harmless cookies, preserves private sessions, and keeps cache hits stable across storefronts.

Multi-Store Magento 2 Varnish VCL With Cookie Stripping
Multi-Store Magento 2 Varnish VCL With Cookie Stripping

If your Magento 2 multi-store setup is behind Cloudflare, Apache SSL proxy, and Varnish on one server, a normal Magento VCL often isn't enough. This guide explains the exact VCL pattern we used to make anonymous storefront pages return stable HIT responses while keeping cart, checkout, customer, admin, API, and private-cookie traffic safe.

The short answer: Magento 2 Varnish Multi-Store VCL works best when the cache key includes the host, protocol, Magento vary value, and device group, while harmless public cookies are stripped before hashing. Private cookies and private routes must always pass. In our case, the first anonymous request returned MISS, the next request returned HIT, cache hits increased correctly, and the measured anonymous TTFB dropped to around 0.106s with a total response time around 0.312s.

What's Actually Happening Here

Magento 2 multi-store Varnish caching fails when Varnish receives every storefront request as if it belongs to the same cache context. In a simple single-store setup, Magento’s exported VCL is often enough. In a single-server multi-store setup with Cloudflare, Apache SSL proxying, and several storefronts, the request context becomes more complicated.

The common path looks like this: Cloudflare receives the public HTTPS request, Apache terminates SSL, Apache forwards the request to Varnish, Varnish fetches from Apache or PHP-FPM through the Magento backend, and Magento returns a full-page cache response. Every layer must preserve the original host and protocol. If the host is wrong, one store can pollute another store’s cache. If the protocol is wrong, Magento can create HTTP and HTTPS cache variations or redirect issues. If cookies are not handled carefully, Varnish will pass too many public pages and your hit rate will stay low.

Adobe’s Commerce documentation recommends Varnish as the full-page cache application for production Magento installations, and Magento’s full-page cache TTL is commonly configured with values such as 86400 seconds, which equals 24 hours. The Adobe Commerce Varnish configuration documentation explains how Varnish sits in front of Magento and how cache purging is controlled through Magento cache invalidation.

The browser made the problem more visible. Anonymous curl requests with an empty cookie header returned Varnish HIT, while browser requests sometimes returned MISS or UNCACHEABLE. The reason was cookie noise. Browser requests carried analytics cookies, Magento local-storage cookies, cache warmer cookies, session cookies, and private-content cookies. Some of those cookies are harmless for public anonymous pages. Some are not.

Cloudflare added another layer of confusion. The response showed cf-cache-status: DYNAMIC, but that did not mean Varnish failed. According to Cloudflare’s cache response documentation, CF-Cache-Status describes Cloudflare’s edge cache state. Magento HTML can be dynamic at Cloudflare while still being served from origin-side Varnish with X-Cache: HIT.

That distinction matters. Cloudflare can be dynamic for HTML, while Varnish can still be working perfectly at origin.

The Solution — Hitori Multi-Store Varnish Pattern

The Hitori Multi-Store Varnish Pattern is simple: separate every storefront at the cache-key level, strip only safe cookies, pass anything private, and make purging host-aware. This keeps the speed benefit of Varnish without risking private content leaks or cross-store cache pollution.

Step 1 is to configure Magento to use Varnish as the full-page cache application.

Use placeholders for the backend host, backend port, and allowed purge hosts based on your own infrastructure.

php bin/magento config:set system/full_page_cache/caching_application 2
php bin/magento config:set system/full_page_cache/ttl 86400
php bin/magento config:set system/full_page_cache/varnish/access_list "<ALLOWED_PURGE_HOSTS>"
php bin/magento config:set system/full_page_cache/varnish/backend_host "<BACKEND_HOST>"
php bin/magento config:set system/full_page_cache/varnish/backend_port "<BACKEND_PORT>"
php bin/magento config:set system/full_page_cache/varnish/grace_period "300"
php bin/magento cache:flush

Step 2 is to size Varnish for the server.

In our production case, the server had enough unused memory and CPU capacity, so Varnish was configured with 16G malloc storage, multiple thread pools, and larger header/workspace limits. That is not a universal setting. A smaller server should use less memory, but the principle is the same: don’t leave Varnish on a tiny default cache if you run many storefronts.

A generic service configuration looks like this:

[Unit]
Description=Varnish Cache, a high-performance HTTP accelerator
Documentation=https://www.varnish-cache.org/docs/ man:varnishd

[Service]
Type=simple

# Maximum number of open files (for ulimit -n)
LimitNOFILE=131072

# Locked shared memory - should suffice to lock the shared memory log
# (varnishd -l argument)
# Default log size is 80MB vsl + 1M vsm + header -> 82MB
# unit is bytes
LimitMEMLOCK=85983232
ExecStart=/usr/sbin/varnishd \
          -j unix,user=vcache \
          -F \
          -a :80 \
          -T localhost:6082 \
          -f /etc/varnish/default.vcl \
          -S /etc/varnish/secret \
          -s malloc,16G \
          -p thread_pools=4 \
          -p thread_pool_min=200 \
          -p thread_pool_max=8000 \
          -p thread_pool_timeout=300 \
          -p http_resp_hdr_len=65536 \
          -p http_resp_size=98304 \
          -p workspace_client=256k \
          -p workspace_backend=256k \
          -p feature=+http2

ExecReload=/usr/share/varnish/varnishreload
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
PrivateDevices=true

[Install]
WantedBy=multi-user.target

Step 3 is to make Apache preserve the public request context.

The SSL virtual host should preserve the original host and send HTTPS headers before proxying into Varnish.

ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
RequestHeader set X-Forwarded-SSL "on"

ProxyPass /.well-known !
ProxyPass / http://127.0.0.1:80/
ProxyPassReverse / http://127.0.0.1:80/

Step 4 is to use a multi-store-safe VCL.

This version avoids hardcoding private server details and does not include the health probe that caused 503 Backend fetch failed in our environment. A probe can be useful in other setups, but it must only be used when the probe URL reliably represents the backend health for the exact same routing context.

vcl 4.1;

import std;

acl purge {
    "localhost";
    "127.0.0.1";
    "::1";
    "<ALLOWED_PURGE_IP>";
}

backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 5s;
    .first_byte_timeout = 600s;
    .between_bytes_timeout = 600s;
    
}

sub vcl_recv {
    # -----------------------------
    # Normalize host
    # -----------------------------
    if (req.http.Host) {
        set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
        set req.http.Host = std.tolower(req.http.Host);
    }

    # -----------------------------
    # Real IP handling: Cloudflare / Apache SSL proxy
    # -----------------------------
    if (req.http.CF-Connecting-IP) {
        set req.http.X-Real-IP = req.http.CF-Connecting-IP;
        set req.http.X-Forwarded-For = req.http.CF-Connecting-IP;
    } else if (req.http.X-Forwarded-For) {
        set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
        set req.http.X-Real-IP = client.ip;
    } else {
        set req.http.X-Real-IP = client.ip;
        set req.http.X-Forwarded-For = client.ip;
    }

    # -----------------------------
    # HTTPS handling
    # Apache 443 proxy should send X-Forwarded-Proto=https
    # Cloudflare also may send CF-Visitor
    # -----------------------------
    if (req.http.CF-Visitor ~ "https") {
        set req.http.X-Forwarded-Proto = "https";
    } else if (req.http.X-Forwarded-Proto) {
        # keep Apache-provided value
    } else {
        set req.http.X-Forwarded-Proto = "http";
    }

    set req.http.X-Forwarded-Host = req.http.Host;
    set req.http.grace = "none";

    # -----------------------------
    # PURGE / BAN
    # Host-based ban for multi-store safety
    # -----------------------------
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return (synth(405, "Method not allowed"));
        }

        if (!req.http.X-Magento-Tags-Pattern && !req.http.X-Pool) {
            return (synth(400, "X-Magento-Tags-Pattern or X-Pool header required"));
        }

        if (req.http.X-Magento-Tags-Pattern) {
            if (req.http.Host && req.http.Host != "127.0.0.1" && req.http.Host != "localhost") {
                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);
            }
        }

        if (req.http.X-Pool) {
            ban("obj.http.X-Pool ~ " + req.http.X-Pool);
        }

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

    # -----------------------------
    # Only GET / HEAD are cacheable
    # -----------------------------
    if (req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "DELETE" &&
        req.method != "PATCH") {
        return (pipe);
    }

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

    # -----------------------------
    # Never cache admin/private/customer/payment/API routes
    # -----------------------------
    if (req.url ~ "^/(admin|backend|x2rodape)(/|$)") {
        return (pass);
    }

    if (req.url ~ "^/(customer|checkout|cart|onestepcheckout|multishipping|sales|wishlist|review/customer|catalog/product_compare|paypal|braintree|stripe|adyen|klarna|amazonpayments|vault|giftcard|reward|storecredit|downloadable|newsletter/manage|captcha|rest|graphql|soap|oauth)(/|$)") {
        return (pass);
    }

    if (req.url ~ "^/customer/section/load") {
        return (pass);
    }

    if (req.http.Authorization) {
        return (pass);
    }

    if (req.url ~ "^/(pub/)?health_check.php$") {
        return (pass);
    }

    # -----------------------------
    # Normalize URL
    # -----------------------------
    set req.url = regsub(req.url, "^http[s]?://", "");

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

    # Do not cache URLs with explicit session/store/cache bypass params
    if (req.url ~ "(\?|&)(SID|PHPSESSID|___store|___from_store|nocache|no_cache|preview|token|form_key|uenc)=") {
        return (pass);
    }

    # -----------------------------
    # Static assets: cache and strip cookies
    # -----------------------------
    if (req.url ~ "\.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|otf|eot|mp4|webm|pdf)(\?.*)?$") {
        unset req.http.Cookie;
        return (hash);
    }

    # -----------------------------
    # Cookie handling
    # This is the most important part for your browser issue.
    # -----------------------------
    std.collect(req.http.Cookie);

    # Save Magento vary before cleaning cookies
    if (req.http.Cookie ~ "X-Magento-Vary=") {
        set req.http.X-Magento-Vary = regsub(req.http.Cookie, ".*X-Magento-Vary=([^;]+).*", "\1");
    }

    # Real private/customer/admin cookies: never cache
    if (req.http.Cookie ~ "(adminhtml|customer_logged_in|customer_group|persistent_shopping_cart|private_content_version)") {
        return (pass);
    }

    # Strip harmless cookies from public anonymous pages
    if (req.http.Cookie) {
        # Analytics / tracking / Cloudflare challenge / Bing / Matomo / GA
        set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_ga|_gid|_gat|_gat_[^=]+|_ga_[^=]+|_uetvid|_uetsid|_pk_id[^=]*|_pk_ses[^=]*|cf_clearance|chessio-matomo|mst-cache-warmer-track)=[^;]*", "");

        # Magento frontend harmless/private-content JS cookies
        set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(form_key|mage-cache-storage|mage-cache-storage-section-invalidation|mage-cache-timeout|mage-cache-sessid|mage-messages|product_data_storage|recently_viewed_product|recently_viewed_product_previous|recently_compared_product|recently_compared_product_previous|searchsuiteautocomplete|section_data_ids)=[^;]*", "");

        # Magento vary and session cookie for anonymous public pages
        # We already stored X-Magento-Vary in req.http.X-Magento-Vary above
        set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(X-Magento-Vary|PHPSESSID)=[^;]*", "");

        # Clean empty separators
        set req.http.Cookie = regsuball(req.http.Cookie, "^;\s*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, ";\s*;", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";\s*$", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^\s*$", "");

        if (req.http.Cookie == "") {
            unset req.http.Cookie;
        }
    }

    # If unknown cookies remain, pass safely instead of caching private content
    if (req.http.Cookie) {
        return (pass);
    }

    return (hash);
}

sub vcl_hash {
    hash_data(req.url);

    # Host separation is critical for your multi-domain Magento setup
    if (req.http.Host) {
        hash_data(req.http.Host);
    } else {
        hash_data(server.ip);
    }

    # Separate http/https cache objects
    if (req.http.X-Forwarded-Proto) {
        hash_data(req.http.X-Forwarded-Proto);
    }

    # Magento customer group / store / currency variation
    if (req.http.X-Magento-Vary) {
        hash_data(req.http.X-Magento-Vary);
    }

    # Optional mobile separation because your theme appears to have mobile/desktop differences
    if (req.http.User-Agent ~ "(?i)(iphone|ipod|android|blackberry|windows phone|mobile)") {
        hash_data("mobile");
    } else {
        hash_data("desktop");
    }
}

sub vcl_backend_response {
    # Store host on object for host-based ban/purge
    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.http.X-Magento-Debug) {
        set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
    }

    # Never cache backend/server errors
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
        return (deliver);
    }

    # During debugging, avoid caching 403/404
    # Later you may cache 404 if desired.
    if (beresp.status == 403 || beresp.status == 404) {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
        return (deliver);
    }

    # Static assets
    if (bereq.url ~ "\.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|otf|eot|mp4|webm|pdf)(\?.*)?$") {
        unset beresp.http.Set-Cookie;
        set beresp.ttl = 30d;
        set beresp.grace = 1d;
        return (deliver);
    }

    # Never cache explicitly private/no-store responses
    if (beresp.http.Cache-Control ~ "private|no-store") {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
        return (deliver);
    }

    # If response has real private/session cookies, do not cache
    if (beresp.http.Set-Cookie ~ "(adminhtml|customer_logged_in|customer_group|persistent_shopping_cart|private_content_version)") {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
        return (deliver);
    }

    # Magento public full page cache response
    if (beresp.http.X-Magento-Tags || beresp.http.X-Magento-Cache-Control ~ "public" || beresp.http.Cache-Control ~ "public") {
        # Important: do not let Set-Cookie make Varnish object uncacheable
        unset beresp.http.Set-Cookie;
        unset beresp.http.Pragma;

        set beresp.ttl = 1d;
        set beresp.grace = 1h;

        return (deliver);
    }

    # If no public cache signal, do not cache
    set beresp.ttl = 0s;
    set beresp.uncacheable = true;
    return (deliver);
}

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

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

sub vcl_deliver {
    if (obj.uncacheable) {
        set resp.http.X-Magento-Cache-Debug = "UNCACHEABLE";
    } else if (obj.hits > 0) {
        set resp.http.X-Magento-Cache-Debug = "HIT";
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Magento-Cache-Debug = "MISS";
        set resp.http.X-Cache = "MISS";
        set resp.http.X-Cache-Hits = "0";
    }

    # Keep these while testing
    # Later, you can hide X-Varnish/Via if you want,
    # but tools detect Varnish using these.
    # unset resp.http.X-Varnish;
    # unset resp.http.Via;
    unset resp.http.X-Magento-Cache-Control;
    unset resp.http.X-Magento-Tags;
    unset resp.http.X-Host;
    unset resp.http.X-Magento-Debug;
    unset resp.http.X-Powered-By;
    unset resp.http.X-Magento-Tags;
    #unset resp.http.X-Host;
}

Step 5 is to validate before restarting.

Varnish lets you compile-check the VCL before putting it into service, which avoids downtime from syntax mistakes.

varnishd -C -f /etc/varnish/default.vcl >/dev/null && echo "VCL OK"
systemctl daemon-reload
systemctl restart varnish
systemctl status varnish --no-pager

Step 6 is to test like an anonymous visitor.

The empty cookie header matters because it tells you whether the public cache path works without browser-specific noise.

URL="https://<YOUR_STOREFRONT_DOMAIN>/"

for i in 1 2 3; do
  curl -skS -o /dev/null -D - \
  -H "Cookie:" \
  "$URL" \
  | egrep -i "HTTP/|cf-cache-status|x-cache|x-cache-hits|x-magento-cache-debug|age|cache-control|x-varnish"
  echo "------"
  sleep 2
done

The expected pattern is MISS, then HIT, then a higher hit count:

HTTP/2 200
cache-control: max-age=86400, public, s-maxage=86400
x-magento-cache-debug: MISS
x-cache: MISS
x-cache-hits: 0
cf-cache-status: DYNAMIC
------
HTTP/2 200
cache-control: max-age=86400, public, s-maxage=86400
age: 2
x-magento-cache-debug: HIT
x-cache: HIT
x-cache-hits: 1
cf-cache-status: DYNAMIC
------
HTTP/2 200
cache-control: max-age=86400, public, s-maxage=86400
age: 4
x-magento-cache-debug: HIT
x-cache: HIT
x-cache-hits: 2
cf-cache-status: DYNAMIC

Step 7 is to separate backend performance from frontend PageSpeed.

Varnish can bring anonymous TTFB down to a very small number, but Lighthouse can still score badly if CSS blocks rendering, JavaScript runs for seconds, images lack dimensions, and third-party scripts load early. In one test, the Varnish TTFB was around 0.106s, but the mobile performance score was still low because the page had render-blocking CSS, high JavaScript execution time, layout shifts, and oversized images. That is a frontend optimization problem, not a Varnish failure.

Multi-Store VCL Compared With Other Approaches

A custom multi-store VCL is the right choice when one Magento codebase serves several storefronts behind Cloudflare and Apache. It gives you host-aware cache safety, better anonymous hit rates, and safer purge behaviour than a generic VCL.

ApproachBest forStrengthRiskOperational effort
Default Magento VCLSimple single-store Magento setupsFast to export and deployCan miss too often when browser cookies are noisyLow
Cloudflare cache everythingStatic sites or very controlled Magento storesVery fast edge deliveryDangerous for cart, checkout, customer, and private AJAX unless rules are perfectMedium to high
Hitori Multi-Store VCLMagento multi-store on one serverHost-aware caching, safe cookie stripping, safe private-route passingRequires careful testing per storeMedium
Separate Varnish per domainLarge isolated storefrontsStrong separationMore services, more memory, more config driftHigh
No Varnish, Magento built-in FPC onlySmall stores or stagingSimple setupHigher backend load and slower anonymous TTFB under trafficLow

The named pattern matters because it makes troubleshooting repeatable. Instead of asking “is cache working?”, you check four exact things: host separation, protocol separation, cookie stripping, and private-route passing.

For broader infrastructure work around Magento, DevOps automation, and performance operations, Hitori Tech’s digital agency and DevOps services are built around this kind of practical production tuning. If your team also uses workflow automation for cache warming, reporting, or deployment checks, our N8N automation services can connect those operational tasks into a clean process.

Common Mistakes and How to Avoid Them

The most common mistake we see is judging Varnish from browser DevTools only. Browsers carry many cookies, and Magento reacts differently when cookies are present. Always test the anonymous path with curl -H "Cookie:" before assuming Varnish is broken.

The second mistake is treating cf-cache-status: DYNAMIC as a bad sign. For Magento HTML, dynamic at Cloudflare can be correct. The safer pattern is often Cloudflare for DNS, TLS, WAF, and static asset delivery, while origin-side Varnish handles Magento full-page HTML caching. Check X-Cache, X-Cache-Hits, X-Magento-Cache-Debug, Age, and X-Varnish.

The third mistake is stripping all cookies without thinking. That can create security problems. The VCL should strip known harmless cookies from public anonymous pages, but it must pass when private cookies remain. Customer, admin, session, authorization, checkout, cart, and customer-section requests should not be cached.

The fourth mistake is adding a health probe without validating the backend route. A Varnish probe is useful only when the probe URL returns reliable status through the same backend context Varnish uses. In our case, the probe caused repeated 503 Backend fetch failed behaviour, so we removed it and validated the backend directly with host-specific requests.

The fifth mistake is using the cache warmer incorrectly. If your warmer excludes all catalog, product, or category URLs, you are warming the wrong things. Magento product and category pages are usually the highest-value anonymous pages to warm. Checkout, cart, customer, private routes, search noise, and query-heavy URLs should be excluded instead.

Real-World Example

The working result was a clean anonymous cache pattern across multiple storefronts on one Magento installation. The first request created the cache object. The second and third requests were served from Varnish.

A representative result looked like this:

HTTP/2 200
cache-control: max-age=86400, public, s-maxage=86400
x-varnish: <REQUEST_ID>
age: 0
x-magento-cache-debug: MISS
x-cache: MISS
x-cache-hits: 0
cf-cache-status: DYNAMIC

Then the next request showed:

HTTP/2 200
cache-control: max-age=86400, public, s-maxage=86400
x-varnish: <REQUEST_ID> <CACHE_OBJECT_ID>
age: 2
x-magento-cache-debug: HIT
x-cache: HIT
x-cache-hits: 1
cf-cache-status: DYNAMIC

Then the next request showed:

HTTP/2 200
cache-control: max-age=86400, public, s-maxage=86400
x-varnish: <REQUEST_ID> <CACHE_OBJECT_ID>
age: 4
x-magento-cache-debug: HIT
x-cache: HIT
x-cache-hits: 2
cf-cache-status: DYNAMIC

The anonymous response timing also confirmed the backend cache layer was no longer the bottleneck:

TTFB: 0.106430
Total: 0.312493

The important lesson is that Varnish and PageSpeed measure different problems. Varnish fixed backend response time. Lighthouse still reported frontend issues such as render-blocking CSS, large JavaScript execution, layout shift from unsized images, unused CSS, and third-party scripts. Once the cache layer is stable, the next work is critical CSS, JavaScript delay, image dimensions, LCP image optimization, font loading, and third-party tag cleanup.

Google’s Lighthouse performance documentation explains that performance scoring is calculated from metrics such as First Contentful Paint, Largest Contentful Paint, Total Blocking Time, Cumulative Layout Shift, and Speed Index. Varnish mainly helps server response time and TTFB. It does not automatically remove render-blocking CSS or reduce JavaScript execution.

Frequently Asked Questions

What is Magento 2 Varnish Multi-Store VCL?

Magento 2 Varnish Multi-Store VCL is a Varnish configuration designed for one Magento installation serving multiple storefront domains. It keeps each store’s cache separate by host, preserves HTTPS context, strips safe public cookies, passes private traffic, and uses host-aware purging so one store does not accidentally affect another.

How do I test if Magento Varnish cache is working?

Test Magento Varnish cache with repeated anonymous requests using an empty cookie header. The first request should usually show MISS, and the next requests should show HIT, increasing X-Cache-Hits, and a growing Age header. Browser tests are useful later, but curl -H "Cookie:" is the cleanest way to prove the anonymous cache path.

Why does Cloudflare show DYNAMIC when Varnish shows HIT?

Cloudflare shows DYNAMIC because Cloudflare did not serve the HTML from its own edge cache. Varnish can still serve the response from origin cache at the same time. For Magento, this is often the safer setup because Cloudflare does not need to cache private-sensitive HTML while Varnish handles Magento full-page caching at origin.

Why does Magento show HIT in curl but MISS in the browser?

Magento can show HIT in curl but MISS in the browser because the browser sends cookies. Some cookies are harmless analytics or frontend-storage cookies, while others represent private session context. The VCL must strip known harmless cookies and pass unknown or private cookies.

Can I use a Varnish health probe with Magento 2?

You can use a Varnish health probe with Magento 2 only if the probe URL reliably returns the right status through your exact backend routing. In this setup, the probe caused backend fetch failures, so it was removed. Always test the backend route directly before enabling a probe in production.

Which Magento pages should never be cached by Varnish?

Checkout, cart, customer account pages, admin URLs, wishlist, payment routes, REST, GraphQL, OAuth, customer section loading, and authorized requests should never be cached. Public CMS pages, product pages, category pages, and static assets can usually be cached when Magento marks the response public and private cookies are handled safely.

A Magento 2 Varnish Multi-Store VCL should be boring once it’s working. The headers should tell the story: first MISS, then HIT, then increasing hit counts across every storefront. If you want help building this kind of stable Magento, Cloudflare, Apache, and Varnish architecture, talk to Hitori Tech through our contact page or explore our full service offering.

Himanshu Verma

Written by

Himanshu Verma

Himanshu is a full-stack developer and SaaS builder behind VerifiSaaS. He shares practical insights on email verification, deliverability, and growth systems to help businesses scale smarter