Varnish in 2026: it's called Vinyl Cache now, and the mental model still holds

Back in 2016 I gave a talk called Varnish Explained and wrote it up slide by slide. It still gets traffic, which is nice, except the version numbers on those slides are now a decade old and the project doesn’t even go by the same name anymore.

So here’s the 2026 version. Two things changed that you need to know about, and one big thing that didn’t change at all.

The name changed: Varnish is Vinyl Cache now#

This is the part that trips people up first, so let’s get it out of the way.

In late 2025 the open source project started renaming itself, and around its 20-year anniversary in early 2026 it became Vinyl Cache. The rename came out of a trademark dispute with Varnish Software, the commercial company. The result is two projects with confusingly similar names:

  • Vinyl Cache, the community open source project (the direct continuation of what you knew as Varnish Cache).
  • Varnish Cache, a separate distribution that Varnish Software keeps shipping downstream.

If you install the open source thing in 2026, you’re getting Vinyl. And the binary doesn’t hide it. Here’s the version banner straight off the container I tested this post on:

$ varnishd -V
varnishd (varnish-9.0.1 revision e8e39111b9d37f0e90130e8c66c53acc9be1d3f4)
Copyright (c) 2006 Verdens Gang AS
Copyright (c) 2006-2026 Varnish Software
Copyright 2010-2026 UPLEX - Nils Goroll Systemoptimierung
Copyright 2026 The Vinyl Cache Project

The command is still varnishd, the binary still reports varnish-9.0.1, but that last copyright line is new. For the rest of this post I’ll mostly say “Varnish”, because that’s still what everyone types and what the binaries are called. Just know the project you’re following for releases and docs is Vinyl now.

The versions moved a lot#

If your knowledge stopped at 4.x or 5.x (which is where my old talk left it), here’s the catch-up as of mid-2026, from endoflife.date :

  • 9.0 shipped March 2026. Latest patch is 9.0.1. This is the current line.
  • 8.0 shipped September 2025, supported until September 2026.
  • 6.0 LTS was the long-lived one, released back in 2018. Its security support ran out in May 2026, so it’s now off the table for new installs.
  • 7.7 went end of life in March 2026.

The short version: if you’re standing up something new, you want 9.x. Everything 7.x and below is either gone or going. The 3.x/4.x/5.x syntax on my old slides will not load on any of these.

What did not change: the whole mental model#

Here’s the good news, and the reason that 2016 write-up still gets read. The engine works exactly the same way.

Varnish is still a reverse proxy that sits in front of your webserver and decides, per request, whether to serve a cached copy or go fetch a fresh one. It still runs your logic through the same routines:

  • vcl_recv to inspect and rewrite the incoming request,
  • vcl_hash to decide the cache key,
  • vcl_backend_response to massage what the backend sent back,
  • vcl_deliver for one last edit before the response goes out.

ESI, grace mode, bans, the cookie-and-cache-key story: all still true. If you understood the flow a decade ago, you understand it today. So I won’t re-explain it here; the old post is still a fine mental model. What I want to give you is config that actually loads on a 2026 install.

A modern VCL that compiles on 9.x#

Here’s a small but real default.vcl. It declares a backend, normalises the query string, strips tracking cookies, allows cache invalidation over PURGE from trusted IPs only, and adds a debug header so you can see hits versus misses. Every line below loaded cleanly with varnishd -C on 9.0.1.

vcl 4.1;

import std;

backend default {
    .host = "backend";
    .port = "80";
}

acl purgers {
    "localhost";
    "127.0.0.1";
}

sub vcl_recv {
    # ?a=1&b=2 and ?b=2&a=1 are the same page; collapse them to one cache object
    set req.url = std.querysort(req.url);

    if (req.method == "PURGE") {
        if (!client.ip ~ purgers) {
            return (synth(405, "Not allowed"));
        }
        return (purge);
    }

    if (req.http.Cookie) {
        set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");
        if (req.http.Cookie == "") {
            unset req.http.Cookie;
        }
    }
}

sub vcl_backend_response {
    # The backend says "Cache-Control: no-cache" and sets a session cookie.
    # Cache it anyway: drop both so the built-in VCL doesn't bail, then set a TTL.
    unset beresp.http.Set-Cookie;
    unset beresp.http.Cache-Control;
    set beresp.ttl = 2m;
}

sub vcl_deliver {
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
    set resp.http.X-Cache-Hits = obj.hits;
}

The first line is vcl 4.1;, which is the current syntax version (not the same thing as the Varnish release number, confusingly). The unset Cache-Control trick in vcl_backend_response is the one piece of real Varnish behaviour worth calling out: if you leave the backend’s Cache-Control: no-cache in place, the built-in VCL marks the object uncacheable and you’ll get a miss every single time. Drop it, set your own beresp.ttl, and Varnish caches it regardless of what the app wanted. That’s the whole “Varnish does what you tell it, not what the backend asks” idea from the old talk, in three lines.

Proving it actually caches#

Validate the config before you load it, every time:

$ varnishd -C -f /etc/varnish/default.vcl

No output that looks like an error means it compiled. Then a couple of requests through it:

$ curl -s -D- -o /dev/null http://localhost:8080/ | grep -i x-cache
X-Cache: MISS
X-Cache-Hits: 0

$ curl -s -D- -o /dev/null http://localhost:8080/ | grep -i x-cache
X-Cache: HIT
X-Cache-Hits: 1

First request fills the cache, second is served from memory. And the query-sort line earns its keep:

$ curl -s -D- -o /dev/null "http://localhost:8080/?a=1&b=2" | grep -i x-cache
X-Cache: MISS
$ curl -s -D- -o /dev/null "http://localhost:8080/?b=2&a=1" | grep -i x-cache
X-Cache: HIT

Different URL, same cache object, because std.querysort sorted the parameters before the hash. Without it, those would be two separate entries and you’d be caching the same page twice.

Flushing the cache: PURGE and bans#

Two ways to invalidate, same as a decade ago. PURGE is the per-URL hammer, gated by that acl purgers so randoms on the internet can’t wipe your cache:

# from a trusted IP
$ curl -s -o /dev/null -w "%{http_code}\n" -X PURGE http://localhost/
200

# from an untrusted IP, the ACL kicks in
$ curl -s -o /dev/null -w "%{http_code}\n" -X PURGE http://example/
405

Bans are the regex-based version, run over the admin interface. They mark matching objects stale without walking the whole cache up front:

$ varnishadm ban 'req.url ~ "^/style"'

$ varnishadm ban.list
Present bans:
1780840341.168432     0 -  req.url ~ ^/style

How I run this in production#

At Oh Dear this isn’t theory. Our status pages sit behind Varnish, and last year we leaned on it hard: we were getting hit by a DDoS pushing roughly a million requests a minute , and Varnish is a big part of why the status pages stayed up. Caddy now proxies to Varnish instead of straight to the backend, which took our status-page response times from 250-300ms down to 30-50ms.

The trick that mattered most under attack was an allowlist in vcl_recv. Instead of trying to block bad URLs, we 404 everything that isn’t a known status-page route, right at the edge, so randomised garbage URLs never reach PHP and never get a chance to bypass the cache:

sub vcl_recv {
    set req.url = std.querysort(req.url);

    # Anything that isn't a known status-page route gets 404'd here,
    # so it never touches the backend.
    if (req.url !~ "^/(\?.*)?$" &&
        req.url !~ "^/(json|xml|rss)(\?.*)?$" &&
        req.url !~ "^/status-page/[a-z0-9-]+(/(json|xml|history|subscribe-slack(/create)?|subscribe-(rss|json|xml)))?(\?.*)?$" &&
        req.url !~ "^/build/assets/[A-Za-z0-9._-]+\.(js|mjs|css|svg|woff2?|png|webp|jpe?g|gif|ico|map)(\?.*)?$" &&
        req.url !~ "^/favicon\.ico$") {
        return (synth(404, "Not a status page route"));
    }

    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    return (hash);
}

That’s lifted from our live config (I swapped the backend IP for a placeholder), and it compiles on 9.x as shown. Pair it with grace mode so a slow or dead backend doesn’t take you down with it:

sub vcl_backend_response {
    set beresp.grace = 3600s;

    # Don't cache or deliver a backend 5xx; keep serving the stale object via grace
    if (beresp.status >= 500) {
        return (abandon);
    }
}

The other half of running a cache is keeping it warm. We use Oh Dear’s own crawler for that: you can authorise the Oh Dear crawler in your VCL and let its checks refresh your cache instead of letting it go cold and serving the first real visitor a slow miss.

Getting started in 2026#

If you’re new to it: install Vinyl/Varnish 9.x from your distro or the project’s repo, point the backend at your existing nginx or Apache, run it on an alternate port, and test before you swap it onto :80. That part of the old getting-started checklist hasn’t aged.

The name on the tin is different and the version jumped a few majors, but VCL is VCL. Validated on Varnish/Vinyl 9.0.1 via the official varnish Docker image, with nginx 1.31.1 as the backend.