Vary header: Accept-Encoding, User-Agent and * (tested)

The Vary response header lists the request headers that change the response, so a cache must only reuse a stored copy when those request headers match. We counted server hits in Chromium 153 for five Vary values to see when the browser cache really reuses a response and when it goes back to the server.

1. The results

Every response had Cache-Control: max-age=60. Numbers are requests that reached the server for that URL; 1 means every later request was answered from Chromium's HTTP cache.

VarySame fetch twiceX-Custom 1, 2, then 1 againUA changed (DevTools override)Accept-Encoding changedBack buttonReload
(none)1111cacheserver
Accept-Encoding1112cacheserver
User-Agent1121cacheserver
X-Custom1311cacheserver
Accept-Encoding, User-Agent1122cacheserver
*2322serverserver

What the numbers show:

2. You cannot change User-Agent or Accept-Encoding from fetch()

We called fetch(url, {headers: {'User-Agent': 'OtherUA/1.0'}}). Chromium 153 silently dropped the header: the server received the normal HeadlessChrome/153.0.0.0 user agent and, with Vary: User-Agent, the response came from cache. Accept-Encoding cannot be set from page JavaScript either; Chromium always sent gzip, deflate, br, zstd. So for a real browser those two Vary values only split the cache when the browser itself changes (DevTools device emulation, a UA override, an update that changes the version string). We changed them with DevTools protocol overrides (Network.setUserAgentOverride, extra headers) to get the columns above.

3. Vary: Accept-Encoding: send it on every response

If a server picks gzip or plain bytes based on Accept-Encoding, the response must say Vary: Accept-Encoding on both branches, or a shared cache can store the gzip version and hand it to a client that cannot decode it. Node.js, checked with curl 8.14.1 (both responses carried Vary: Accept-Encoding, only the first had Content-Encoding: gzip):

const http = require('http');
const zlib = require('zlib');
http.createServer((req, res) => {
  const body = JSON.stringify({ hello: 'world' });
  const gzip = /\bgzip\b/.test(req.headers['accept-encoding'] || '');
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Cache-Control', 'public, max-age=60');
  res.setHeader('Vary', 'Accept-Encoding'); // on BOTH branches
  if (gzip) {
    res.setHeader('Content-Encoding', 'gzip');
    res.end(zlib.gzipSync(body));
  } else {
    res.end(body);
  }
}).listen(8967);

nginx (config only, not run in this test): gzip_vary on; adds Vary: Accept-Encoding to responses that the gzip module could compress.

gzip on;
gzip_vary on;
gzip_types text/css application/javascript application/json image/svg+xml;

If you append to Vary yourself, merge values into one header (Vary: Accept-Encoding, Origin) rather than overwriting what a framework already set.

4. Vary: User-Agent and Vary: *: usually avoid

Documentation, not measured here: shared caches and CDNs key on the full header value, and real User-Agent strings vary per browser version and OS, so Vary: User-Agent splits a CDN cache into many near-identical copies or makes it skip caching. Some CDNs only honour a short list of Vary values and treat others as uncacheable; check your CDN's docs. If you must serve different HTML per device, prefer separate URLs or client hints with a small value set. For Vary: *, RFC 9110 says the response cannot be matched by a later request without contacting the origin, which is what we measured in Chromium. If you want "do not reuse", say it directly with Cache-Control: no-cache or no-store.

5. Vary: Origin

For CORS responses that echo the request origin, the important value is Vary: Origin; we measured that case with an nginx cache in Vary: Origin, CORS and caching.

Frequently asked questions

What does the Vary header do?

It names the request headers that change the response, so a cache only reuses a stored response when those request headers match. In Chromium 153, Vary: X-Custom made a request with a different X-Custom value go back to the server.

Does Vary: Accept-Encoding stop the browser from caching?

No. With the same browser the Accept-Encoding value never changes, and a response with Vary: Accept-Encoding and max-age=60 was fetched once and then served from cache, same as no Vary.

What does Vary: * mean?

That no later request can be assumed to match. Chromium 153 never reused a Vary: * response: two identical fetches both hit the server and the back button refetched the page.

Should I use Vary: User-Agent?

Rarely. It does not hurt one browser's cache (the UA does not change), but documented CDN behaviour is to split or skip caching on it because User-Agent strings are so varied. Prefer separate URLs or client hints.

Does Chrome cache several Vary variants of one URL?

No, not in our test. Alternating X-Custom 1, 2, 1 against Vary: X-Custom cost three server hits: each new variant replaced the previous one.

Can I change User-Agent with fetch()?

Not in Chromium 153: a User-Agent header passed to fetch() was dropped and the server saw the normal browser UA. Accept-Encoding cannot be set from page JavaScript either.

Written with AI assistance. Measured on 2026-10-02 in full Chromium 153.0.8010.12 (Playwright, channel 'chromium', one fresh browser context per Vary value) against a Node.js 24.21.0 server on localhost:8965-8967 that counted every request per URL and answered with Cache-Control: max-age=60 plus the Vary value under test. Each case used its own URL. The nginx snippet and the CDN notes are documentation, not executed. PlainHeaders is our extension; Node.js, Chromium and the HTTP headers tool are free.