Years ago I posted a handful of Varnish 3.0 one-liners for varnishtop and varnishlog
. They were all variations on the same idea: pick a log tag like RxURL or TxURL, then grep for what you wanted.
Every one of those commands is broken on a current Varnish. Not “slightly different”, broken. Varnish 4.0 rewrote the shared-memory log from scratch, renamed every tag, and added a real query language so you don’t have to grep at all. I’m on Varnish/Vinyl 9.0.1 for this post, and here’s how varnishlog actually works now.
What changed: tags got renamed, and grep got replaced#
Two things happened in the 4.0 rewrite that you have to internalise.
First, the old Rx*/Tx* tags (receive/transmit) are gone. They were ambiguous because “receive” meant different things on the client side and the backend side. The modern tags name the role instead:
Req*is the request as received from the client (ReqMethod,ReqURL,ReqHeader).Bereq*is the backend request Varnish sends upstream after your VCL has had its way with it (BereqMethod,BereqURL,BereqHeader).Resp*is what Varnish sends back to the client,Beresp*is what the backend sent to Varnish.
So RxURL became ReqURL, TxURL became BereqURL, RxHeader became ReqHeader. That alone fixes most of the old one-liners.
Second, and this is the bigger deal: every transaction now has a VXID (a transaction id), log records are grouped, and there’s a query language (-q) that filters on the actual fields. You almost never need to pipe varnishlog into grep anymore.
Grouping: -g request#
By default varnishlog shows you a wall of records. The -g flag groups them, and -g request is the one you’ll use most: it shows a full request as one block, with the linked backend fetch indented underneath. Here’s a real cache hit on the homepage:
$ varnishlog -g request
* << Request >> 9
- ReqURL /
- VCL_call RECV
- VCL_return hash
- VCL_call HASH
- VCL_return lookup
- Hit 3 109.472803 10.000000 0.000000
- VCL_call HIT
- VCL_return deliver
- RespHeader X-Varnish: 9 3
- RespHeader Age: 10
- RespHeader X-Cache: HIT
- RespHeader X-Cache-Hits: 3
- End
You can read the whole decision in one place: request comes in, vcl_recv returns hash, the lookup finds an object (Hit), and it’s delivered from cache. The Age: 10 says the object is 10 seconds old. That’s the kind of thing you used to reconstruct by hand from scattered log lines.
The other grouping modes are -g session (everything on one client connection), -g vxid (the default-ish per-transaction view) and -g raw (no grouping, firehose). Request is the sweet spot for debugging.
The four old one-liners, rewritten#
Here’s the 2011 post, modernised.
Top URLs requested by clients. Old: varnishtop -i RxURL. New, just the renamed tag:
$ varnishtop -1 -i ReqURL
6.00 ReqURL /
3.00 ReqURL /?a=1&b=2
2.00 ReqURL /style.css
2.00 ReqURL /my-unique-page
1.00 ReqURL /?b=2&a=1
(-1 prints once and exits instead of running as a live “top”. Drop it for the interactive view.)
Top URLs fetched from the backend. Old: varnishtop -i TxURL. New:
$ varnishtop -1 -i BereqURL
2.00 BereqURL /
2.00 BereqURL /?a=1&b=2
1.00 BereqURL /style.css
1.00 BereqURL /my-unique-page
Notice the counts are lower than the client side. That’s the cache doing its job: clients asked for / six times, but only two of those reached the backend.
Cookies clients are sending. Old: varnishtop -i RxHeader -I Cookie. New: varnishtop -i ReqHeader -I Cookie, same shape.
Which Host is hit most. Old: varnishtop -i RxHeader -I '^Host:'. New: varnishtop -i ReqHeader -I '^Host:'.
The part that replaces grep: -q queries#
This is the real upgrade. Instead of dumping everything and grepping, you write a query that filters on the fields themselves. Want only the requests that got a 405?
$ varnishlog -g request -i ReqMethod,ReqURL,RespStatus -q 'RespStatus == 405'
* << Request >> 14
- ReqMethod PURGE
- ReqURL /
- VCL_return synth
- RespStatus 405
There’s the rejected PURGE from an untrusted IP, and nothing else. The -q expression does the filtering; the -i just trims which tags get printed.
Queries understand operators (==, ~ for regex, eq, >, and so on), so you can filter on a URL pattern, a client IP, a status, a header value, whatever:
# only requests to a specific path
$ varnishlog -c -q 'ReqURL ~ "/status-page/"'
# only client requests from one IP
$ varnishlog -c -q 'ReqStart[1] eq "10.0.1.5"'
# only backend fetches that returned 5xx
$ varnishlog -b -q 'BerespStatus >= 500'
The ReqStart[1] syntax indexes into a multi-field record (field 1 of ReqStart is the client IP). That field-indexing is the thing the old grep approach could never do cleanly.
Spotting query normalisation#
When your VCL does std.querysort(req.url), the log shows both the original and the rewritten URL, because req.url gets set twice:
$ varnishlog -c -i ReqMethod,ReqURL
* << Request >> 32773
- ReqMethod GET
- ReqURL /?b=2&a=1
- ReqURL /?a=1&b=2
The client asked for ?b=2&a=1, Varnish sorted it to ?a=1&b=2, and now it shares a cache object with every other ordering. Being able to see both values inline is how you confirm a rewrite is doing what you think.
When you want plain access logs: varnishncsa#
If you just want Apache/nginx-style access logs out of Varnish, varnishncsa formats the same log stream into the combined log format:
$ varnishncsa
192.168.65.1 - - [07/Jun/2026:13:50:56 +0000] "GET http://localhost:8080/ HTTP/1.1" 200 28 "-" "curl/8.7.1"
192.168.65.1 - - [07/Jun/2026:13:50:56 +0000] "GET http://localhost:8080/?a=1&b=2 HTTP/1.1" 200 36 "-" "curl/8.7.1"
192.168.65.1 - - [07/Jun/2026:13:50:56 +0000] "PURGE http://localhost:8080/ HTTP/1.1" 405 252 "-" "curl/8.7.1"
It takes the same -q queries, so varnishncsa -q 'RespStatus >= 500' gives you an error-only access log. Run it as a service if you want Varnish writing access logs to disk continuously.
Save now, debug later: -w and -r#
The one workflow I reach for constantly: capture the binary log during an incident, analyse it afterwards without the pressure of a live stream scrolling past. Write with -w, replay with -r:
# capture while the problem is happening
$ varnishlog -w /tmp/incident.binlog
# later, slice it however you like; the file holds the raw records
$ varnishlog -r /tmp/incident.binlog -g request -q 'RespStatus >= 500'
$ varnishtop -r /tmp/incident.binlog -1 -i ReqURL
$ varnishncsa -r /tmp/incident.binlog
Every command in this post that shows output was actually run against a replayed capture exactly like this. The binary file keeps all the tags, so you can re-query it as many times as you want with different -q expressions.
This is also how we debug Oh Dear
’s status-page cache: when something looks off, grab a capture, then work through it with varnishlog -g request -q '...' until the bad transaction shows itself. If you’re newer to all of this, the original Varnish mental model
still holds; only the tooling moved on.
Validated on Varnish/Vinyl 9.0.1 via the official varnish Docker image.