Response header
< Vary: Accept-Encoding, Accept-LanguageVary Header: Cache Keys, Vary: Origin and Accept-Encoding
Vary lists the request headers a response depends on, so caches keep separate copies. Vary: Accept-Encoding, Vary: Origin for CORS, Vary: *, and CDN cache keys.
- Direction
- Response
- Category
- Caching
- Spec
- RFC 9110 §12.5.5
On this page
TL;DR:
Varytells caches which request headers changed the response, so one URL can have several cached variants. SetVary: Accept-Encodingwhen you compress andVary: OriginwheneverAccess-Control-Allow-Origindepends on the request.
How a cache key really works
A cache keys on the method and URL, plus every request header named in the stored response’s Vary (RFC 9111 section 4.1). Two requests that differ only in an unlisted header share an entry.
GET /app HTTP/1.1
Accept-Encoding: br
HTTP/1.1 200 OK
Content-Encoding: br
Vary: Accept-Encoding
A later request with Accept-Encoding: gzip does not match, so the cache goes to the origin and stores a second variant. Matching is on the exact header values, so CDNs usually normalise Accept-Encoding first to avoid needless variants.
Headers you will actually vary on
Accept-Encoding. Compression. Servers add it automatically in nginx (gzip_vary on;) and Expresscompressionmiddleware.Origin. Dynamic CORS. See below.Accept. API content negotiation (application/jsonvstext/html), and image formats such asimage/avif.Accept-Language. Language redirects without separate URLs; better to use/en/paths.AuthorizationorCookie. Personalised responses. Better to mark themCache-Control: privatethan to Vary; varying onCookiefragments a shared cache into one entry per user.User-Agent. Almost always a mistake. Use Client Hints or responsive CSS instead.
Vary: Origin and CORS cache poisoning
If your server echoes the caller’s origin:
Access-Control-Allow-Origin: https://app.example.com
then add:
Vary: Origin
Without it a CDN or the browser cache can store the response for https://app.example.com and serve it to https://admin.example.com, whose browser then logs:
The 'Access-Control-Allow-Origin' header has a value 'https://app.example.com' that is not equal to the supplied origin.
It often looks intermittent and disappears with a hard refresh. Also vary on origin even when you do not send an ACAO header for disallowed origins, otherwise a no-CORS response can be cached and reused for a CORS request. See Access-Control-Allow-Origin.
Vary: *
Vary: * says the response depends on factors outside request headers, such as client IP. Under RFC 9111 a cache never treats a stored response as a match without validation. It is blunt; Cache-Control: no-store or private states your intent better.
Configuration
nginx:
gzip on;
gzip_vary on; # adds Vary: Accept-Encoding for compressed responses
location /api/ {
add_header Vary Origin always;
}
nginx add_header Vary ... appends another Vary line rather than merging; that is allowed, as caches combine them. Check the combined result with curl.
Express:
res.vary('Origin') // appends instead of overwriting
res.vary('Accept-Encoding')
Cloudflare: Vary support is limited (see the FAQ). To cache by origin, Accept or other headers, use a custom cache key via Cache Rules (Enterprise) or key on a URL parameter. CloudFront: forward the headers in the cache policy; they become part of the cache key. A Vary header that names a header CloudFront does not forward is a common cause of wrong content.
Verify
curl -sI https://example.com/app -H 'Accept-Encoding: br' | grep -iE '^(vary|content-encoding|age|x-cache|cf-cache-status)'
curl -sI https://api.example.com/data -H 'Origin: https://app.example.com' | grep -i '^vary'
Mistakes
- Missing
Vary: Acceptwhen one URL serves JSON or HTML by negotiation, leaving the CDN to return whichever it cached first. Vary: Cookieon public pages that merely read a tracking cookie; the hit rate drops to near zero.- Setting
Varyin the app but having a proxy overwrite it. Inspect at the client, not just the origin. - Using
Vary: User-Agentfor mobile detection. Prefer one responsive page.
Related
Frequently asked questions
What does the Vary header do?
It lists request header names that influenced the response. A cache must store a separate variant per distinct value of those headers and reuse a stored response only if the new request matches on every listed header.
Why do I need Vary: Accept-Encoding?
The same URL returns different bytes for gzip, Brotli or identity. Without Vary: Accept-Encoding a shared cache can serve a compressed body to a client that did not ask for it, or the reverse. Most servers add it automatically when compressing; CDNs normalise the header into a small number of variants.
Why do I need Vary: Origin for CORS?
If Access-Control-Allow-Origin echoes the request Origin, the response differs per origin. Without Vary: Origin a cache can serve the copy generated for origin A to origin B, producing a CORS error that vanishes on hard refresh. Add it whenever the allowed origin is dynamic.
What does Vary: * mean?
It means the response depends on something beyond request headers, so no stored response can be matched to a later request. Caches may store it but must revalidate every time. Prefer Cache-Control: no-store when you want to stop caching.
Why is my CDN cache hit rate low with Vary?
Each distinct value of a varied header creates a new cache entry. Vary: User-Agent or Vary: Cookie explodes into thousands of variants. Vary only on headers with a few values, or normalise them at the edge into buckets.
Does Cloudflare honour Vary?
Not generally. On most plans Cloudflare ignores Vary except Accept-Encoding, and supports Vary for images only in specific configurations. Do not rely on Vary: Origin to protect an edge-cached CORS response; include the origin in a custom cache key or bypass caching for those paths.
Sources
Related
Content Negotiation
Content negotiation lets one URL serve different formats, languages or encodings based on Accept headers. See q-values, Vary, 406 and the caching traps.
Age Header
Learn how the Age header indicates how long a response has been cached in seconds. Understand cache freshness calculations and CDN behavior.
ETag Header: Strong vs Weak, If-None-Match and 304 Debugging
ETag identifies a resource version for cache validation and optimistic locking. Strong vs weak ETags, If-None-Match, If-Match, and why nginx gzip weakens them.
Expires Header
Learn how the Expires header specifies when cached responses become stale. Understand date formats and when to use Expires vs Cache-Control for caching.