Custom Varnish VCL Rules: Boost Hit Rate & Reduce Load
Default Varnish configuration often works inefficiently: it caches everything or fails to cache critical pages, ignores authorization headers, and breaks on edge cases with cookies. The result is a low hit rate (30–40%) and excessive backend load. If you're facing this issue, request an audit of your current Varnish configuration. With over 7 years of experience and 50+ projects focused on optimization, we guarantee an increase in hit rate to 90%+ while preserving full business logic. One of our clients—an e-commerce store with 500,000 requests per hour—saw their hit rate jump from 35% to 93% in just 2 days after implementing custom rules. This reduced backend load by 8x and saved up to 70% on server costs.
Varnish Cache uses VCL to describe caching policies.
How Custom VCL Rules Accelerate Your Site?
Custom VCL rules address specific needs: conditional cache control, request normalization, bypassing CDN for certain routes, routing to different backends, and grace mode when the origin is down. Let's dive into key aspects.
Request normalization
Without normalization, the same resource is stored under dozens of keys: /?utm_source=google, /?utm_source=facebook—these are separate cache entries even though the content is identical. Statistics show that URLs with utm tags waste up to 30% of cache space. Normalization solves this.
vcl 4.1;
import std;
import directors;
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 2s;
.first_byte_timeout = 60s;
.between_bytes_timeout = 10s;
.probe = {
.url = "/healthz";
.timeout = 1s;
.interval = 5s;
.window = 5;
.threshold = 3;
}
}
backend api {
.host = "127.0.0.1";
.port = "8081";
.connect_timeout = 1s;
.first_byte_timeout = 30s;
}
sub vcl_recv {
# Remove marketing parameters
if (req.url ~ "(\\?|&)(utm_source|utm_medium|utm_campaign|utm_term|utm_content|fbclid|gclid|yclid|_ga|mc_eid)=") {
set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_term|utm_content|fbclid|gclid|yclid|_ga|mc_eid)=[^&]*", "");
set req.url = regsuball(req.url, "\\?(utm_source|utm_medium|utm_campaign|utm_term|utm_content|fbclid|gclid|yclid|_ga|mc_eid)=[^&]*&", "?");
set req.url = regsub(req.url, "\\?$", "");
}
# Normalize Accept-Encoding
if (req.http.Accept-Encoding) {
if (req.url ~ "\\.(jpg|jpeg|png|gif|webp|gz|tgz|bz2|tbz|mp3|ogg|swf|flv|mp4|woff2?)$") {
unset req.http.Accept-Encoding;
} else if (req.http.Accept-Encoding ~ "br") {
set req.http.Accept-Encoding = "br";
} else if (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else {
unset req.http.Accept-Encoding;
}
}
# Normalize cookies
if (req.http.Cookie) {
set req.http.Cookie = ";" + req.http.Cookie;
set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
set req.http.Cookie = regsuball(req.http.Cookie, ";(session|auth_token|XSRF-TOKEN)=", "; \\1=");
set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
if (req.http.Cookie == "") {
unset req.http.Cookie;
}
}
# Device detection for adaptive caching
if (req.http.User-Agent ~ "(?i)mobile|android|iphone|ipod|blackberry|opera mini|iemobile") {
set req.http.X-Device-Type = "mobile";
} else {
set req.http.X-Device-Type = "desktop";
}
# Route by content type
if (req.url ~ "^/api/") {
return(pass);
}
if (req.http.Authorization || req.http.Cookie ~ "auth_token=") {
return(pass);
}
if (req.method != "GET" && req.method != "HEAD") {
return(pass);
}
if (req.url ~ "\\.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)(\\?.*)?$") {
unset req.http.Cookie;
return(hash);
}
return(hash);
}
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
}
hash_data(req.http.X-Device-Type);
return(lookup);
}
What is grace mode and how does it work?
Grace mode allows serving stale cache while the backend is overloaded or temporarily unavailable. This is critical for high-traffic sites.
sub vcl_backend_response {
set beresp.grace = 24h;
if (beresp.status >= 500) {
set beresp.ttl = 0s;
set beresp.grace = 60s;
return(deliver);
}
# Custom TTL by content type
if (bereq.url ~ "^/news/") {
set beresp.ttl = 10m;
} else if (bereq.url ~ "^/static/") {
set beresp.ttl = 30d;
unset beresp.http.Set-Cookie;
} else if (bereq.url ~ "^/product/") {
set beresp.ttl = 1h;
} else {
set beresp.ttl = 5m;
}
if (beresp.http.Cache-Control ~ "no-store|private") {
set beresp.uncacheable = true;
return(deliver);
}
}
sub vcl_hit {
if (obj.ttl >= 0s) {
return(deliver);
}
if (obj.ttl + obj.grace > 0s) {
return(deliver);
}
return(restart);
}
How to implement cache invalidation via VCL?
Tag-based purge (via xkey) is the right approach for CMS with object dependencies.
import xkey;
acl purge_acl {
"127.0.0.1";
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purge_acl) {
return(synth(405, "Not allowed"));
}
return(purge);
}
if (req.method == "XKEY-PURGE") {
if (!client.ip ~ purge_acl) {
return(synth(405, "Not allowed"));
}
set req.http.n-gone = xkey.softpurge(req.http.xkey-purge);
return(synth(200, "Purged " + req.http.n-gone + " objects"));
}
}
sub vcl_backend_response {
if (beresp.http.Surrogate-Key) {
set beresp.http.xkey = beresp.http.Surrogate-Key;
}
}
Comparison of invalidation methods:
| Method | Speed | Granularity | Best for |
|---|---|---|---|
| PURGE by URL | Instant | High (single URL) | Individual updates |
| PURGE by tags (xkey) | Instant | Medium (all objects with tag) | Bulk updates (categories) |
| Full cache flush | Requires warm-up | Low (entire cache) | Rare global changes |
Debugging and monitoring
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
set resp.http.X-Served-By = server.hostname;
unset resp.http.X-Powered-By;
unset resp.http.Server;
unset resp.http.X-Varnish;
unset resp.http.Via;
}
Commands for debugging VCL
Monitoring via varnishstat and varnishlog:
# Current hit rate
varnishstat -f MAIN.cache_hit,MAIN.cache_miss
# Live log filtered by URL
varnishlog -q 'ReqURL ~ "^/news/"' -g request
# Top cache miss by URL
varnishtop -i ReqURL -q 'VCL_call eq "MISS"'
Why request normalization is critical for hit rate?
Request normalization is the foundation of efficient caching. Without it, hit rate can be below 50% even with properly configured TTL. For example, URLs with utm tags waste up to 30% of cache space. Normalization also prevents issues with cookie-dependent content and incorrect caching of dynamic pages. Custom VCL rules boost hit rate by 2–3x compared to default configuration, as confirmed by our projects.
How to set up request normalization?
- Identify parameters to remove (utm, fbclid, gclid, etc.).
- Write
vcl_recvthat strips these parameters fromreq.url. - Normalize
Accept-Encoding—prioritize br, then gzip. - Process cookies: remove all but session-essential ones.
- Add device-aware hashing for mobile/desktop separation.
- Test with
varnishtestand monitoring.
Implementation process
A typical custom VCL rules project includes the following stages:
- Analysis (1–2 days): audit current traffic, analyze backend response headers, identify non-cacheable patterns (cookies, Cache-Control: private).
- Design (1 day): develop VCL rule architecture, define cache keys, grace periods, and routing.
- Implementation (2–3 days): write VCL scripts, configure health checks, integrate with CDN.
-
Testing (1 day): validate on staging with
varnishtest, measure hit rate, compare with baseline. - Deployment (1 day): roll out to production, set up monitoring, document.
Complex cases (A/B testing via Varnish, ESI includes, multi-level caching with Nginx) add 3–5 days.
Scope of work
- Custom VCL rules tailored to project architecture.
- URL, cookie, and Accept-Encoding normalization.
- Grace mode and stale-while-revalidate.
- Cache invalidation via PURGE/xkey.
- Integration with CDN and deployment systems.
- Load testing and monitoring setup (varnishstat, metrics).
- Documentation of implemented rules and invalidation procedures.
Comparison: default config vs custom VCL
| Parameter | Default configuration | Custom VCL rules |
|---|---|---|
| Hit rate | 30–40% | 85–95% |
| URL normalization | No | Full (utm, fbclid, excess cookies) |
| Grace mode | None | Configurable (24h+) |
| Invalidation | Only full flush | PURGE by URL and tags |
| Device-aware caching | No | Yes (separate objects for mobile/desktop) |
| Backend load | High | Reduced 5–10x |
Request an audit of your current Varnish configuration—we'll identify bottlenecks and propose custom solutions. Contact us for a tailored offer.







