HTTP Vary Header Reference

HTTP Vary header usage with CDN caching implications and common field names.

Reference for HTTP Vary header semantics covering Accept-Encoding, Accept, Cookie and Origin usage, cache-key fragmentation risks and CDN normalisation guidance. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

What does the Vary header do?

Vary lists the request headers a cache must include in its cache key. If a response says Vary: Accept-Encoding, the cache stores a separate entry for each distinct Accept-Encoding value, so a gzip client never receives an identity-encoded body and vice versa.

The header that decides what your cache stores

Vary is the single most misunderstood caching header. It tells every cache — the browser, a CDN, a reverse proxy — which request headers must be part of the cache key. Get it right and content negotiation works through caches; get it wrong and you either serve the wrong variant or shred your hit rate. This reference rates the common Vary field names by safety and cache impact.

How Vary builds the cache key

When a response includes Vary, a cache stores one entry per distinct combination of the listed request-header values:

Vary: Accept-Encoding, Accept-Language

A request only matches a stored entry if its values for those headers match. So varying on Accept-Encoding keeps gzip and Brotli bodies separate (correct), while varying on Cookie or User-Agent fragments the cache into near-unique entries (harmful). CDNs often normalise values — collapsing dozens of Accept-Encoding strings down to gzip/br/identity — to keep the key small.

Field-by-field guide

FieldUseCache impact
Accept-EncodingSeparate compressed variantsLow — CDNs normalise to a handful of values
Accept-LanguageServe localised contentMedium — keep languages few or use URL-based routing instead
AcceptServe different content types (JSON vs HTML)Low for APIs; prefer separate URLs for high-traffic content
OriginCORS preflight cachingLow — only a handful of origins per resource
CookiePer-session personalisationVery high — effectively uncacheable; never use on shared assets
User-AgentServe browser-specific markupVery high — thousands of distinct strings; use CDN normalisation or UA groups
*Varies on something outside headersUncacheable by shared caches; use sparingly

The CDN normalisation difference

Browsers send Vary exactly as written. CDNs add a layer of intelligence: many automatically normalise Accept-Encoding into a small set of canonical values (br, gzip, identity) so the cache does not fragment on minor encoding string variations. Check your CDN’s documentation to understand what it normalises — a field that fragments the cache badly in an origin cache may be safe on a CDN that handles it.

Tips and pitfalls

  • Always send Vary: Accept-Encoding for compressed responses so caches do not serve an encoded body to a client that only accepts plain text.
  • Avoid Vary: Cookie and Vary: User-Agent on cacheable assets — they reduce cache hit rates toward zero.
  • Vary: Origin is the correct way to handle CORS; it ensures each origin’s preflight response is cached independently.
  • Vary: * makes a response effectively uncacheable by shared caches — use it only when negotiation depends on something the cache genuinely cannot see.
  • Keep the list as short as possible; each additional field multiplies the number of distinct stored variants and divides your cache hit rate accordingly.

Why cache fragmentation compounds

The damage from over-varying is multiplicative, not additive. Each field the cache keys on multiplies the number of stored variants by the number of distinct values that field takes:

Vary listDistinct values seenVariants storedEffect
Accept-Encoding~3 (br/gzip/identity)3Negligible
Accept-Encoding, Accept-Language3 × 2060Manageable
Accept-Encoding, Cookie3 × ~1 per user~3 per userCache effectively dead

A single object cached under Vary: Cookie becomes a per-user object — the shared cache degrades to a private cache, and origin load rises to what it would be with no cache at all. This is why the fix for a “why is my CDN hit rate 4%?” incident is almost always a stray Vary: Cookie or Vary: User-Agent emitted by a framework default.

Correct pairing with Cache-Control

Vary decides which variant a request maps to; Cache-Control decides whether and how long any variant is stored. They must agree: marking a response Cache-Control: private alongside Vary: Accept-Encoding still keeps it out of shared caches, so the Vary is moot. For a compressible, publicly cacheable asset the correct pairing is Cache-Control: public, max-age=... plus Vary: Accept-Encoding and nothing else.

Debugging a Vary problem in five minutes

The fastest diagnostic: request the same URL twice with different values of the header you suspect (for example Accept-Encoding: gzip then identity) and compare the Age and cache-status headers of the responses. A cache serving one variant to both requests means Vary is missing; a cache that never serves a hit at all on a personalised endpoint often means an over-broad Vary: Cookie or Vary: * is fragmenting the cache into one-request-per-variant. Both failure modes are invisible in a single request — always test in pairs.

A closing rule of thumb: every value you add to Vary multiplies the number of cache entries a URL can occupy, so treat each addition as a deliberate trade-off between correctness and hit ratio — normalise header values at the edge where you can, and prefer one well-chosen variant key to three accidental ones.

Sources