HTTP Vary and Cache Keys: Reuse Rules, Vary: *, and Cache Fragmentation

Vary adds request headers to the cache key. Per RFC 9110 and 9111: when header values match, why Vary: * forces revalidation every time, and how content negotiation splits a cache.

httpcachevarycontent-negotiation

Two requests for the same URL: one is answered straight from cache, the other goes to the origin server every time. If the response carries Vary, the cause is usually the value of a request header that Vary points at. This post explains how Vary changes cache reuse, using the relevant sections of RFC 9110 (HTTP Semantics, June 2022) and RFC 9111 (HTTP Caching, June 2022). It does not cover how any particular CDN or browser implements this.

Directives that decide whether a response may be stored and reused at all, such as no-cache and no-store, are covered in Cache-Control: no-cache vs no-store. Vary answers a different question: given a stored response, may it be used for the request that just arrived?

Where the cache key starts: method and URI

RFC 9111 Section 2 defines the cache key as the information a cache uses to choose a response, made up of at least the request method and the target URI. The same section adds that many caches in common use today only store GET responses, so in practice they key on the URI alone.

That breaks down when one URI returns different content for different requests. If /index sends a gzip-compressed body to clients that send Accept-Encoding: gzip and the plain body to everyone else, the URI cannot tell the two responses apart. RFC 9111 Section 2 says caches handle this by adding some of the original request’s header fields to the cache key, and the response’s Vary field says which ones.

What Vary says

Per RFC 9110 Section 12.5.5, Vary describes which parts of the request, other than the method and target URI, might have influenced how the origin server selected the content of this response. Its value is either * or a list of request header field names. The RFC calls the listed fields the selecting header fields.

HTTP/1.1 200 OK
Vary: accept-encoding, accept-language

This response says the origin server might have used the request’s Accept-Encoding and Accept-Language values, or their absence, when choosing the content. Field names are case-insensitive (RFC 9110 Section 5.1), so accept-encoding and Accept-Encoding are the same name.

Section 12.5.5 gives a list-valued Vary two purposes:

  1. For caches: they MUST NOT use this response for a later request unless that request has the same values for the listed fields as the original request, or the origin server has validated the reuse. In the RFC’s words, Vary expands the cache key.
  2. For user agents: this response went through content negotiation, and sending different values in the listed fields might get a different representation.

When two requests’ headers count as “the same”

RFC 9111 Section 4.1 says that when a stored response has Vary, the cache MUST NOT reuse it without revalidation unless every request header nominated by that Vary matches the original request. Matching is a little broader than comparing raw strings. Two requests’ fields match if one can be turned into the other using only these transformations:

  • adding or removing whitespace where the field’s syntax allows it
  • combining multiple field lines with the same name into one
  • normalizing both values in a way the field’s specification says has identical meaning, for example reordering values whose order does not matter, or case-normalizing values defined as case-insensitive

One more rule: if, after any normalization, a field is absent from a request, that request can only match another request where the field is also absent.

Say a response with Vary: Accept-Language was stored from a request that sent Accept-Language: ko-KR,ko;q=0.9. The table applies the rules above directly. It is not something observed in a particular cache product.

Later request’s Accept-Language Reuse without revalidation Reason
ko-KR,ko;q=0.9 Yes Same value
ko-KR, ko;q=0.9 Yes Only whitespace after the list separator differs
(field absent) No An absent field only matches an absent field
ko No Different value, even if the server returns the same Korean page
en-US,en;q=0.9 No Different value

The whitespace difference is a transformation the RFC names, so it counts as a match. A case like ko versus ko;q=1, which mean the same thing once you apply the default quality value (RFC 9110 Section 12.4.2: no q means 1), may fall under the third transformation, normalization known to preserve meaning. The RFC only allows that normalization, so whether a given cache treats the two as matching depends on the implementation.

If several stored responses match, the cache has to pick one. Section 4.1 says that when a field has a way to rank preference, such as quality values on Accept-style fields, the cache MAY use it; otherwise, or on a tie, it picks the most recent response by the Date header. If nothing matches, the cache cannot answer the request and typically forwards it to the origin, possibly with conditional headers describing what it already has stored.

How content negotiation fragments a cache

The ko row is the important one. Matching is decided by request header values, not by what the server actually sent back. So even if the server returns the identical Korean page for many different Accept-Language values, a wide spread of values from clients means the same content gets stored as separate entries, or requests fail to find a matching entry and go to the origin. This follows directly from the Section 4.1 rules.

RFC 9110 acknowledges the cost. Section 12.1 lists, among the disadvantages of proactive (server-driven) negotiation, that it limits the reusability of responses for shared caching. The same section notes that servers may also pick a representation from implicit characteristics such as the client’s network address or parts of User-Agent. Once a field is named in Vary, its entire value is compared, so the more distinct values a field takes, the finer the cache is split. That sentence is a derivation from the rules; the sources for this post contain no measurements of how much fragmentation occurs.

Dropping Vary is not automatically the fix. Section 12.5.5 says an origin server SHOULD generate Vary on a cacheable response when it has tailored the content to the selecting fields and wants the response to be reused selectively. It also says Vary might be left out when the origin considers the variance in content selection less significant than Vary’s performance impact on caching, especially when cache directives already limit reuse. Either way, the call belongs to the origin server.

What Vary: * means

A Vary containing * signals that things beyond the request headers, possibly outside the message entirely, such as the client’s network address, might have played a role in choosing the representation (RFC 9110 Section 12.5.5). As a result, a recipient cannot tell whether the response fits a later request without forwarding that request to the origin server. RFC 9110 Section 12.4.3 puts it as the variance being unlimited.

The cache rule is shorter. RFC 9111 Section 4.1 says a stored response whose Vary contains * always fails to match. Combined with the first rule in Section 4.1, such a response can never be reused for any request without revalidation. That sentence does not forbid storing the response, so reuse after the origin validates it is still possible.

A few details:

  • If * appears anywhere in the list, that meaning applies. Vary: Accept-Encoding, * is treated as *. RFC 9110 Appendix B.3 records that the meaning of * alongside other values was clarified in this revision.
  • A proxy MUST NOT generate * in a Vary value. The restriction is placed on proxies, not on origin servers.

What you don’t need to put in Vary

RFC 9110 Section 12.5.5 gives two examples:

  • Authorization: reusing a response to a request with this field for a different user is already prohibited by the field’s definition (Section 11.6.2), so there is no need to list it. RFC 9111 Section 3.5 also says a shared cache may only reuse a response to a request with Authorization if the response carries a directive such as must-revalidate, public, or s-maxage.
  • Network region: if content was chosen by region but the origin wants the cached response reused even when users move between regions, it does not have to signal that variance in Vary.

The RFC also calls out the opposite mistake: leaving Vary off the default response, the one sent when the client expresses no preference. Without it, a cache may pick that default response even when a better-fitting one is stored. RFC 9111 Section 4.1 says that when some stored responses for a URI lack Vary, the cache SHOULD choose the most recent stored response that has a valid Vary value.

Vary in revalidation and 304

Vary is also used on the revalidation path:

  • When a cache generates a validation request on its own, it synthesizes one from a stored response by copying the method, the target URI, and the request header fields named by Vary (RFC 9111 Section 4.3.1).
  • A server sending 304 (Not Modified) MUST generate any of Content-Location, Date, ETag, Vary, Cache-Control, and Expires that it would have sent in a 200 response to the same request (RFC 9110 Section 15.4.5).
  • A cache receiving 304 first identifies the stored responses that could have been selected for the request. If the 304 contains a strong validator, it updates the headers of the responses within that set that share the validator (RFC 9111 Section 4.3.4).

This is where ETag design matters. If the origin uses strong ETags, representations with different representation data must not share one. RFC 9110 Section 8.8.3.3 illustrates this with distinct tags for the plain and gzip-coded forms of /index: the plain response has ETag: "123-a", the gzip response has ETag: "123-b", and both carry Vary: Accept-Encoding. The section explains that content codings are a property of the representation data, so the strong entity tag of an encoded representation has to differ from that of the unencoded one.

HTTP/1.1 200 OK
ETag: "123-b"
Vary: Accept-Encoding
Content-Encoding: gzip

Checks when designing responses

  • Does Vary list only the request headers the server actually reads when choosing content? Listing headers it ignores only removes reuse opportunities.
  • On a negotiated URI, does the default response carry the same Vary?
  • Is Vary: * really needed? Once it is set, every reuse goes through origin validation.
  • Do 304 responses carry the same Vary and ETag as the 200?
  • If strong ETags are used, do representations with different encoded or language content have distinct values?

Sources

Checked: 2026-10-10

Back to the blog