Back to HTTP Headers

Vary caching response

Tells caches which request headers affect the response — so they store separate cached versions per unique combination.

What it does

Vary tells caches that the response is not one-size-fits-all — it varies based on the value of certain request headers. A cache seeing Vary: Accept-Encoding knows it must store separate cached versions for clients that support gzip, clients that support br, and clients that support neither.

Without Vary, a cache might serve a gzip-compressed response to a client that doesn't understand gzip — resulting in garbled content. With Vary: Accept-Encoding, the cache keys responses by encoding support and always serves the right version.

Syntax

Vary: <header-name>
Vary: <header-name>, <header-name>
Vary: *

Examples:

Vary: Accept-Encoding
Vary: Accept-Encoding, Accept-Language
Vary: Accept
Vary: *

How cache keying works

Without Vary, a cache key is just the URL. With Vary, the cache key becomes URL + the values of the listed request headers.

For Vary: Accept-Encoding, the cache stores up to one entry per unique Accept-Encoding value:

  • Accept-Encoding: gzip, br → one cached entry (compressed)
  • Accept-Encoding: identity → a different cached entry (uncompressed)
  • No Accept-Encoding header → another entry

When a new request arrives, the cache looks for a stored response with a matching Vary combination. No match → cache MISS, fetch from origin.

The most important use case: compression

Vary: Accept-Encoding is the single most common use of this header and is essentially mandatory for any server that serves compressed responses:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Encoding: gzip
Vary: Accept-Encoding
Cache-Control: public, max-age=3600

Without Vary: Accept-Encoding here, a CDN might cache the gzip-compressed version and serve it to a client that didn't send Accept-Encoding: gzip — that client receives raw gzip bytes it can't decode.

Most web servers (nginx, Apache, Caddy) and CDNs add Vary: Accept-Encoding automatically when they compress responses. But if you're compressing in application code, you're responsible for setting it.

Other common uses

Content negotiation by language:

Vary: Accept-Language

If the same URL serves English or French based on the Accept-Language request header, this tells caches to store separate versions per language. Required for correct multilingual caching.

Content negotiation by format:

Vary: Accept

If the same URL serves JSON or XML based on the Accept header, caches must store separate versions.

Both encoding and language:

Vary: Accept-Encoding, Accept-Language

Cache key = URL + encoding + language. For 2 encodings × 3 languages = 6 possible cached variants.

The Vary: * nuclear option

Vary: *

This means "the response varies based on something you can't predict from request headers." The practical effect: the response is uncacheable by shared caches. Every request goes to the origin. This is the correct response when something like cookies or authentication determines the response and there's no safe way to cache it.

Don't use Vary: * as a lazy "just don't cache this" — use Cache-Control: no-store or private for that. Vary: * is semantically different: it says "this response is potentially cacheable but the variation factor is opaque."

CDN considerations

Vary is well-supported by modern CDNs but with nuances:

  • Cloudflare normalises Accept-Encoding (maps many variations to a small set) and supports Vary: Accept-Encoding well. Other Vary fields may be ignored or handled differently.
  • Fastly has explicit Vary support but recommends controlling cache keys with their custom Surrogate-Key or Vary configuration.
  • AWS CloudFront requires explicit "cache key policy" configuration to vary on headers — setting Vary on the response alone isn't sufficient; you must configure CloudFront to include those headers in its cache key.

Always verify your CDN's Vary handling against their docs — assumed behaviour here can cause hard-to-debug caching bugs.

Common mistakes and gotchas

Omitting Vary: Accept-Encoding on compressed responses. The most common real-world Vary bug. Compressed responses without this can serve garbage to non-gzip clients.

Vary on high-cardinality headers. Vary: User-Agent would create thousands of cache entries (one per unique user agent string) — effectively destroying cache hit rate. Only vary on headers with a small set of meaningful values.

Vary: Cookie nuking CDN caching. If your response genuinely varies by cookie (authenticated content), use Cache-Control: private instead of Vary: Cookie. Most CDNs ignore or mishandle Vary: Cookie anyway, and private clearly communicates "don't cache in shared caches."

Forgetting Vary on language-negotiated pages. A common SEO trap: your server returns Content-Language: fr for French users at the same URL, but omits Vary: Accept-Language. A CDN might cache the first response (say, French) and serve it to all users, regardless of their Accept-Language. All your English users get French content, with no indication of what went wrong.

Real-world examples

Compressed HTML with language negotiation:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Language: en
Content-Encoding: gzip
Vary: Accept-Encoding, Accept-Language
Cache-Control: public, max-age=300

JSON API with format negotiation:

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Cache-Control: public, max-age=60

Private authenticated response:

HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: private, max-age=300

No Vary needed — private already prevents shared cache storage.

FAQ

Does Vary affect browser caching?

Yes. Browsers maintain their own cache and respect Vary too. A browser receiving Vary: Accept-Encoding will store the response keyed by encoding. In practice, a single browser always sends the same Accept-Encoding, so Vary: Accept-Encoding rarely creates multiple browser-side entries — but the semantics still apply.

What if a cache doesn't understand Vary?

If a cache doesn't understand Vary, it should not cache the response. Most modern caches handle Vary correctly, but older HTTP/1.0 proxies might ignore it — another reason to keep caching infrastructure updated.

Can I use Vary with Cache-Control: no-store?

Technically you can include both, but it's contradictory. no-store says "don't cache this at all"; Vary only matters if caching happens. In practice, no-store wins.

Fun fact

Vary was one of HTTP/1.1's most underappreciated additions. In the HTTP/1.0 era, content negotiation (serving different versions of the same URL based on request properties) was common — but caches had no way to know that two requests for the same URL should get different responses. This caused massive cache poisoning incidents in the mid-1990s where proxy caches served wrong-language or uncompressed content to users. Vary was specifically designed to fix this, and it's been quietly preventing these bugs ever since.