Vary
A single URL often produces different responses based on Compression, language, or device type. The Vary response header tells caches which request headers influenced the response, so the correct representation gets stored and served.
Usage
A single URL often produces different responses depending on what the client sends. A server compressing with gzip for one client and Brotli for another serves two distinct representations of the same resource. The Vary header records which request headers caused the server to choose a particular representation, giving caches the information needed to store and match the right copy.
When a cache receives a request for a URL with a stored response, the cache compares the listed Vary headers between the new request and the stored one. If the values match, the cached response is valid. If they differ, the cache fetches a fresh response from the origin.
The most common values are Accept-Encoding (for
Compression negotiation) and Accept (for
content negotiation). Dynamic
serving setups serving different HTML to mobile and
desktop clients based on the
User-Agent string add
Vary: User-Agent to signal this variation.
The wildcard value * signals the response depends on
factors outside the request headers, such as the
client's IP address. A stored response with Vary: *
never matches a later request, so a cache has to
revalidate with the origin every time before reuse.
This is rarely appropriate, and many caches skip
storing these responses altogether.
How caches match variants
A cache identifies a stored response first by the request method and URL. This primary key finds every stored copy of the resource. The headers named in Vary then act as a secondary key: the cache reads those request headers from the new request and picks the stored variant whose original request carried the same values. One URL holds as many variants as there are distinct combinations of those header values.
The variation is driven by the response. A cache only learns which headers matter after the origin answers, because the Vary list arrives on the response. A URL whose responses carry no Vary header stays a single cache entry, no matter how much the incoming request headers differ. A custom cache key built from request headers works the other way around: the key splits every response on those headers upfront, including responses where the content never changes.
When several stored variants match a request and no ranking such as quality values applies, the cache picks the most recent one based on the Date header. Changing the Vary value on the origin does not clear variants already stored under the old list. Those entries stay until they expire or get purged.
Every cacheable response for the same URL needs the same Vary list, including error pages and fallback responses. A 404 or a default-language fallback sent without Vary gets stored as the single copy for every client, and the cache serves the fallback in place of the negotiated content.
Normalization
Two requests with identical preferences often send different header strings. Browsers, operating systems, and user settings format the same preference in different ways.
Accept-Language: en-US,en;q=0.9
Accept-Language: en-us, en;q=0.9
A cache comparing these byte for byte stores two variants for the same content. Caches are allowed to treat header values as matching after removing whitespace where the syntax permits, combining repeated header lines, reordering values where order carries no meaning, and folding case where the value is case-insensitive. Language tags and media types are case-insensitive, so both requests above reduce to the same key.
Application-aware normalization goes further. A site
offering English, French, and German produces three
responses, while Accept-Language
arrives in thousands of distinct forms. Mapping each
request to one of the three supported languages, and
reducing a regional tag like en-US to en when no
separate en-US version exists, keeps the variant
count at three. This mapping depends on knowing which
representations the origin serves, so the cache or CDN
needs the list of supported values as configuration.
Caches and CDNs handle each varied header in one of three ways:
- Normalize. Reduce the header value to a canonical form or a small set of classes before matching. Best for Accept, Accept-Language, and Accept-Encoding.
- Exact match. Compare the raw header bytes. Needed when the application depends on precise tokens, and safe only for headers with few distinct values.
- Skip caching. Send every request to the origin when the header has too many values to cache usefully.
The number of variants multiplies across headers. Ten possible values in one header produce ten variants. Ten values in each of three headers produce 1,000.
Directives
Accept-Encoding
Indicates the response varies by the Accept-Encoding request header. Different Compression algorithms (gzip, br, zstd) produce different response bodies for the same resource. This is the most widely used Vary value.
Accept
Indicates the response varies by the Accept request header. Content negotiation delivers different media types (HTML, JSON, XML) from the same URL based on client preference.
Accept-Language
Indicates the response varies by the Accept-Language request header. Multilingual sites serving localized content from the same URL include this value.
User-Agent
Indicates the response varies by the User-Agent request header. Dynamic serving configurations deliver different HTML to mobile and desktop clients from the same URL.
Cookie
Vary: Cookie means the response differs based on
the Cookie header value. Caches store a
separate variant for each unique Cookie string,
causing significant cache fragmentation since every
authenticated user has a different session cookie.
Restrict Vary: Cookie to responses that genuinely
depend on cookie values (personalized pages,
dashboards) and avoid setting the value on public
static assets.
The same applies to any header carrying per-user or
per-session values: Authorization,
session IDs, API keys, and custom token headers. Each
distinct value creates a separate cache entry used by
one client, and those entries push reusable responses
out of the cache. Shared caches already refuse to
reuse a stored response for a request carrying
Authorization unless Cache-Control
includes public, s-maxage, or must-revalidate. For these headers, mark the response
Cache-Control: private or skip shared caching rather
than listing the header in Vary.
* (wildcard)
The wildcard value signals the response varies by
factors not captured in a request header. A stored
response with Vary: * never matches a later request
and needs revalidation with the origin before every
reuse. Rarely used intentionally.
Example
A server compresses responses using the encoding
requested by the client. The Vary: Accept-Encoding
header tells intermediate caches to store separate
copies for gzip and Brotli requests rather than serving
a gzip-compressed response to a Brotli-capable client.
Vary: Accept-Encoding
A content negotiation setup delivers JSON or HTML from the same endpoint. The cache stores separate responses based on the Accept header value.
Vary: Accept
Multiple headers influencing the response are listed as a comma-separated value. Here the response depends on both the accepted encoding and the preferred language.
Vary: Accept-Encoding, Accept-Language
A dynamic serving configuration delivers different HTML to mobile and desktop user agents. The cache stores separate versions keyed by the full User-Agent string.
Vary: User-Agent
A lower-cardinality alternative varies on the Sec-CH-UA-Mobile client hint, which carries only two values. Only Chromium-based browsers send the hint, so requests from other browsers arrive without the header and form a third variant, typically served the desktop layout.
Vary: Sec-CH-UA-Mobile
Pairing Vary with Cache-Control controls both what varies and how long each variant stays fresh.
Cache-Control: public, max-age=3600
Vary: Accept-Encoding
Troubleshooting
Caching problems caused by a missing or overly broad Vary header often surface as users receiving wrong content.
CDN serving the wrong cached variant. A server returns different content based on a request header but omits the matching Vary value. The CDN caches the first response and serves the same copy to all clients regardless of their request headers. Add the relevant header name to the Vary list. Test by sending requests with different header values through curl and comparing the
X-CacheorAgeresponse headers:curl -H "Accept-Encoding: gzip" -v https://cdn.example.re/style.csscurl -H "Accept-Encoding: br" -v https://cdn.example.re/style.cssCDN ignoring the Vary header. Support for Vary in shared caches is uneven. Some CDNs honor only
Accept-Encodingby default and ignore other values unless a cache rule, custom cache key, or edge function enables variation. The origin sends a correctVary: Accept-Language, but the CDN still serves one language to everyone. Check the CDN documentation for Vary handling and repeat the curl comparison above with the header in question.Cache fragmentation from
Vary: User-Agent. The User-Agent string has thousands of unique values. Listing User-Agent in the Vary header creates a separate cache entry for nearly every visitor, destroying cache hit rates. Some CDNs map User-Agent to device classes through configuration or edge code, and caching on the device class keeps the variant count small. Where possible, vary on Sec-CH-UA-Mobile or a CDN-provided device-type header instead of the raw User-Agent.Too many headers in the Vary list. Hit ratio drops and origin load climbs because each request lands on a new variant. Varying on four or more request headers is common on production sites, and each added header multiplies the variant count. List only the headers the server reads when building the response. Check the current list with
curl -sI https://www.example.re/ | grep -i varyand remove entries added by frameworks or plugins without a matching code path.Fallback or error responses missing Vary. The negotiated pages send
Vary: Accept-Language, but the default-language fallback or a 404 page omits the header. The cache stores the fallback as the only copy and serves the fallback to every client. Send the same Vary list on every cacheable response for the URL, including errors.Vary: *disabling caching entirely. The wildcard tells caches that the response varies by factors outside any request header. A stored copy never matches a later request, so caches either skip storage or revalidate with the origin on every request. RemoveVary: *unless the intention is to disable all caching for the resource. If a specific request header drives the variation, name the header explicitly.Missing
Vary: Origincausing CORS cache poisoning. A server returns a dynamic Access-Control-Allow-Origin value based on the Origin request header but does not includeVary: Origin. A CDN caches the response with one origin and serves the same CORS header to a different origin, causing browsers to block the response. AddVary: Originto every response that reflects a dynamic origin value. To diagnose, send two curl requests from different origins and check whether the cached Access-Control-Allow-Origin changes:curl -H "Origin: https://a.example.re" -v https://api.example.re/datacurl -H "Origin: https://b.example.re" -v https://api.example.re/dataVary: Accept-Encodinginconsistencies between gzip and Brotli. Some origin servers compress with gzip but a CDN edge re-compresses to Brotli, causing a mismatch between the cached encoding and the Vary key. Ensure the origin and the CDN agree on encoding behavior. In nginx, setgzip_vary on;to addVary: Accept-Encodingautomatically. When using Brotli alongside gzip, confirm both modules add the same Vary value so the cache stores separate entries per encoding.
See also
- RFC 9110: HTTP Semantics - Vary
- RFC 9111: HTTP Caching - Calculating Cache Keys with Vary
- No-Vary-Search
- Cache-Control
- Accept-Encoding
- Accept
- Accept-Language
- Sec-CH-UA-Mobile
- Client hints
- Content Negotiation
- Caching
- HTTP headers