Reading the Varnish log in 2026: varnishlog -q, -g grouping and the VSL query language

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.