A long time ago I wrote a Varnish tip about seeing which cookies get stripped in your VCL
. The idea is still one of the most useful things you can do with varnishlog: compare what the browser sent against what reached your backend, so you can confirm your cookie-stripping rules do what you think.
The technique holds up. The commands in that old post don’t. It used Varnish 3 tags (RxURL) and the -m TAG:regex filter, both gone now. Here’s the same trick on Varnish/Vinyl 9.0.1.
Why you strip cookies at all#
Quick refresher. Cookies are part of the cache key by default (they go through vcl_hash). If every visitor has a unique session or tracking cookie, every request hashes differently and your cache hit rate collapses to zero. So a typical VCL strips the cookies that don’t matter for caching (analytics, JS feature flags) and keeps the ones that do.
For this post the VCL strips has_js and any __utm* cookie, but leaves a my-cookie in place:
sub vcl_recv {
if (req.http.Cookie) {
set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");
set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");
}
}
Now, did it work? Don’t guess. Watch it.
What the client sent#
Use varnishlog -c (client side) with a -q query to pin down the one request you care about. The old post filtered by hand; now you write a query against the URL field:
$ varnishlog -c -i ReqMethod,ReqURL,ReqHeader -q 'ReqURL ~ "/my-unique-page"'
* << Request >> 11
- ReqMethod GET
- ReqURL /my-unique-page
- ReqHeader Host: localhost:8080
- ReqHeader User-Agent: curl/8.7.1
- ReqHeader Cookie: my-cookie=2; has_js=1; __utma=123456789; __utmb=987654321
Four cookies came in from the client: my-cookie, has_js, __utma, __utmb.
What reached the backend#
Same request, but varnishlog -b (backend side) and the Bereq* tags. This is what Varnish forwarded after the VCL ran:
$ varnishlog -b -i BereqMethod,BereqURL,BereqHeader -q 'BereqURL ~ "/my-unique-page"'
* << BeReq >> 12
- BereqMethod GET
- BereqURL /my-unique-page
- BereqHeader Host: localhost:8080
- BereqHeader User-Agent: curl/8.7.1
- BereqHeader Cookie: my-cookie=2;
There’s the proof. has_js, __utma and __utmb are gone; only my-cookie=2; made it through. The two changes from the old post: -c versus -b picks client or backend, and ReqURL versus BereqURL is the renamed tag pair (Req* is from the client, Bereq* is to the backend).
Watch it happen in one transaction#
If you want to see the actual stripping play out, -g request shows every mutation in order. Each regsuball produces a ReqUnset (old value) followed by a ReqHeader (new value):
$ varnishlog -g request -q 'ReqURL ~ "/my-unique-page"'
- ReqHeader Cookie: my-cookie=2; has_js=1; __utma=123456789; __utmb=987654321
- VCL_call RECV
- ReqUnset Cookie: my-cookie=2; has_js=1; __utma=123456789; __utmb=987654321
- ReqHeader Cookie: my-cookie=2; __utma=123456789; __utmb=987654321
- ReqUnset Cookie: my-cookie=2; __utma=123456789; __utmb=987654321
- ReqHeader Cookie: my-cookie=2;
- VCL_return pass
You can read the cookie shrinking line by line: first has_js disappears, then both __utm* cookies go, leaving my-cookie=2;. That’s exactly the before/after the old post was trying to show, except now it’s one grouped view instead of two separate commands.
The built-in VCL gotcha#
Look at that last line: VCL_return pass. The request was not cached.
That’s not a bug in the stripping. It’s the built-in VCL. After your vcl_recv runs, Varnish appends its own built-in logic, and one of the first things it checks is: does this request still have a Cookie header? If yes, it returns pass, meaning “don’t cache, go straight to the backend”. My VCL left my-cookie=2; in place, so the built-in VCL took one look and bypassed the cache.
This catches people constantly. You carefully strip the tracking cookies, you confirm with varnishlog that only your one real cookie reaches the backend, and you still get a 0% hit rate, because that one remaining cookie is enough to trigger the built-in pass. The varnishlog -g request view is how you catch it: if you see VCL_return pass on a page you expected to cache, a leftover cookie is usually why.
If the remaining cookie doesn’t affect the page, the fix is to remove it from the cache decision too (strip it, or unset req.http.Cookie once you’ve decided the page is cacheable). If it does affect the page, then pass is correct and you shouldn’t be caching that request anyway.
Where I use this#
At Oh Dear
our status pages are public and cookie-free by design, so the cache decision stays simple. The heavier lifting in our VCL is routing and an allowlist that 404s anything that isn’t a real status-page route, which is what kept the pages up during a DDoS last year
. The same varnishlog -c / -b split is how I check that routing: see what the client asked for, see what Varnish forwarded, confirm they match what the VCL intended.
For the broader varnishlog toolkit (grouping, the -q query language, save-and-replay), I wrote that up separately in reading the Varnish log in 2026
. And the original Varnish mental model
still explains the cookie-and-cache-key relationship if you want the why.
Validated on Varnish/Vinyl 9.0.1 via the official varnish Docker image, with nginx 1.31.1 as the backend.