Varnish Cache in Front of a Site
Introduction¶
Varnish is a reverse-proxy cache that sits between clients and your web app, serving cached responses from memory at very high rates. It speaks HTTP only (no TLS — put Nginx/HAProxy in front for HTTPS), and is configured in its own language, VCL.
Topology¶
client --HTTPS--> [ Nginx/HAProxy: TLS terminate ] --HTTP:80--> [ Varnish ] --HTTP--> [ app ]
Varnish also often does HTTP→HTTPS awareness via an X-Forwarded-Proto header
the TLS terminator sets.
The VCL lifecycle (simplified)¶
vcl_recv— request arrives. Decide: cache lookup (hash), pass (pass— proxy but don't cache),pipe, orsynth(return a canned response). Normalise the request here (strip tracking cookies, lowercase Host).vcl_hash— build the cache key (default: URL + Host). Add to it here.vcl_hit/vcl_miss— object found / not found.vcl_backend_response— response from the app. Decide TTL, whether to cache, stripSet-Cookie, setgrace/keep.vcl_deliver— final touches before sending to the client (add debug headers).
A starter VCL¶
vcl 4.1;
backend default {
.host = "127.0.0.1";
.port = "8080";
.probe = {
.url = "/healthz";
.interval = 5s;
.timeout = 2s;
.window = 5;
.threshold = 3;
}
}
sub vcl_recv {
# Don't cache non-idempotent methods
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Logged-in users / carts: bypass
if (req.http.Authorization || req.http.Cookie ~ "session=") {
return (pass);
}
# Strip cookies for cacheable content
unset req.http.Cookie;
return (hash);
}
sub vcl_backend_response {
# App said private/no-store? honour it
if (beresp.http.Cache-Control ~ "(?i)(no-store|private)") {
set beresp.uncacheable = true;
return (deliver);
}
# Default TTL for HTML if the app didn't set one
if (beresp.ttl <= 0s) {
set beresp.ttl = 5m;
}
# Ignore app cookies on cacheable responses
unset beresp.http.Set-Cookie;
# Serve stale for up to 6h if the backend is sick
set beresp.grace = 6h;
}
sub vcl_deliver {
set resp.http.X-Cache = obj.hits > 0 ? "HIT" : "MISS";
}
Cookies — the eternal problem¶
Any Set-Cookie in a response, or a request Cookie, makes Varnish not
cache by default. That's why anonymous pages don't cache until you explicitly
unset req.http.Cookie for content that's safe, and unset beresp.http.Set-Cookie
on cacheable responses. Be surgical: keep the session cookie for the paths that
need it (/account, /checkout, /api), strip it elsewhere.
Vary¶
Vary: Accept-Encoding is fine (Varnish handles gzip). Vary: Cookie or
Vary: User-Agent explode the cache into near-infinite variants — normalise or
strip those headers in vcl_backend_response.
Purging and banning¶
- PURGE — invalidate one exact object:
sub vcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge_acl) { return (synth(405)); } return (purge); } }curl -X PURGE http://varnish/path/to/page - Ban — invalidate by pattern (regex on URL/host), lazily applied on next
lookup:
Bans accumulate; use
varnishadm "ban req.http.host == example.com && req.url ~ ^/products/"ban obj.http.x-url ~ ...with the "ban lurker" enabled so old objects are cleaned proactively.
Grace mode¶
beresp.grace keeps an expired object around; if the backend is down or slow,
Varnish serves the stale copy while refreshing in the background. This is
Varnish's headline resilience feature — tune grace generously.
Verification and troubleshooting¶
varnishstat # hit rate (cache_hit / (cache_hit + cache_miss)), evictions, backend health
varnishlog -g request -q 'ReqURL ~ "/products"' # full lifecycle for matching requests
varnishtop -i ReqURL # most-requested URLs
varnishadm backend.list
- Hit rate near zero — cookies (request or
Set-Cookie),Cache-Control: private/no-cachefrom the app,Vary: Cookie, or short/zero TTL.varnishlogshowsTTLandHit-For-Passdecisions. - Users see each other's account pages — you
unset req.http.Cookietoo broadly. Exclude/account,/cart,/api, and anything with a session. 503 Backend fetch failed— backend down and no grace object, or the.probemarks it unhealthy.varnishadm backend.list.- Stale content forever — no purge/ban in the deploy, or bans not being applied (ban lurker disabled and objects never re-looked-up).
- Memory pressure / evictions high (
n_lru_nuked) — cache smaller than the working set. Raise-s malloc,<size>(or use-s file), shorten TTLs on bulky low-value objects. - Compression served twice / broken gzip — let Varnish handle it; don't have
both Nginx and the app and Varnish all gzipping.
Accept-Encodingnormalisation invcl_recv.
Related tools and reading¶
- On-site: HTTP Header Analyzer.
- Related posts: Nginx content caching, An Nginx reverse proxy with TLS.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Web & Mail Servers
Running BIND 9 as an Authoritative Server
BIND as an authoritative server: zone files, secured AXFR with TSIG, SOA serial rules, and DNSSEC inline-signing. Here's a working master/slave setup.
October 24, 2026Let's Encrypt with Certbot, Including Renewal That Doesn't Break
Getting the first cert is easy; keeping renewal working for years is the part people get wrong. Webroot vs DNS-01, deploy hooks, and the failure modes.
October 24, 2026HTTP Basic Auth with htpasswd on Nginx and Apache
The right way to put a password in front of a staging site or an internal endpoint — bcrypt hashes, per-location rules, and an IP-or-password combination.
October 24, 2026