The response header, Vary, has been called “the ugliest part of HTTP that we haven’t yet improved.” The same post describes it as a “horrible, kludgy mechanism” with “pretty abysmal interoperability” across intermediaries. That is usually where sensible engineers back away slowly with their hands raised.
That’s not exactly an endorsement of Vary, but ugly doesn’t mean useless.
One URL can have more than one correct response. A server might, for example, deliver different image formats to different browsers. If a cache ignores Vary, it risks serving the wrong bytes to a request. But if it treats every raw header value as distinct, a handful of similar requests can spread into thousands of barely reusable cache entries. Vary tells a cache which request fields may affect the response, but it does not tell the cache which differences actually matter.
Vary support is now available in Cache Rules on every plan. The origin still names the request headers that may affect a response, but you decide how Cloudflare handles each one. You can normalize known negotiation headers, pass exact values through when those small differences matter, or bypass cache when the variation is too unpredictable. The origin declares what may vary, and you decide how much variation is actually meaningful for the cache.
How Vary works
Vary is a standard HTTP response header that tells intermediary caches (like Cloudflare) which request fields may affect the response sent by the origin. Sites use Vary to serve different languages, image formats, compression schemes, or regional content from the same URL.
Take one URL that produces two valid representations. A browser requests a webpage:
GET /catalog HTTP/1.1
Host: example.com
Accept: text/htmlThe origin returns HTML and identifies Accept as a field that may affect the response:
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=3600
Vary: AcceptAn API client can request the same URL with a different preference:
GET /catalog HTTP/1.1
Host: example.com
Accept: application/jsonThis time, the correct response is JSON. The Vary: Accept header tells the cache that the URL alone is not enough to choose between responses. The request’s Accept value must also be considered.
Without Vary, whichever response enters the cache first can be served to both clients. If HTML wins, the API client receives markup and its JSON parser fails. If JSON wins, a browser expecting a web page receives an API response.
Vary prevents the cache from serving the wrong response to the requesting client. But it introduces a harder question: when two requests contain different header values, do they actually need different responses?
When correct caching becomes useless
Vary can tell a cache which request fields may affect a response. It does not tell the cache what the response represents. For example, take an origin that serves content in only English, French, and German. A client might send:
Accept-Language: en-US, fr;q=0.8While another client might request:
Accept-Language: fr;q=0.8, en-GBBoth requests prefer English here. The origin’s response may map both requests to exactly the same English response. But a cache comparing the raw values cannot safely assume they are equivalent. They have different orders and language tags (that the origin doesn’t differentiate). So the cache may store them as separate variants, even when their response bodies contain identical bytes.
This is Vary’s central problem. Applications often produce a small, finite set of representations from an enormous set of possible request values. The origin understands that thousands of language preferences collapse into three supported languages, while a cache usually does not.
This problem compounds when a response varies on multiple fields. Ten possible values across one field create ten variants. Ten values across three fields can create 1,000 combinations. Real headers can have far greater cardinality: User-Agent values are numerous, cookies can be unique to individual visitors, and preference headers can differ in ordering, formatting (spaces and tabs matter!), and quality values.
The result is a cache that can be perfectly correct and almost permanently cold (an entry never reused). Identical responses can be scattered across entries that receive too little traffic to remain hot and in cache. They can consume capacity, evict one another, reduce cache hit ratios, and send more requests back to origin servers. Eviction can remove cold entries, but it cannot merge them just because the responses are identical.
An analysis of more than 120 million responses from nearly 50,000 popular sites found almost 3,000 sites varying on four or more fields. Some varied on 10, 23, or even 47 fields. We want to make sure that customers have the tools they need to use Vary when appropriate, but not so much that they create a useless cache.
Some high-cardinality variation is deliberate. CDNs or reverse proxies may inject values, such as a geographic region, to partition content predictably. That works when the possible values are controlled and every component agrees on their meaning. Without those constraints, the cache fragments into variants it may never reuse.
That was the design problem we needed to solve to support Vary. We needed to preserve enough variation to serve the right response, without allowing incidental differences between requests to destroy cache efficiency.
How Cache Rules control Vary
Cloudflare customers already had several ways to handle negotiated content similar to Vary. They could bypass cache and let their origin deal with it, reproduce the origin’s negotiation logic in a custom cache key or other rule, use a Worker, or use features like Vary for images.
Those options remain useful, but they either give up caching, duplicate application logic, need to write additional code, or address a narrower use case. Vary in Cache Rules may fill the gap between these existing features by splitting support into two decisions:
- The origin uses
Varyto identify the request headers that may affect a response. - The Cache Rule determines how Cloudflare handles the value of each header.
A Cache Rule does not force every response to vary. If the origin does not return Vary, Cloudflare caches the response normally, though the rule may still rewrite Accept and Accept-Language before forwarding the request to the origin.
When the origin does return Vary, Cloudflare uses the configured action for each header it names. Headers without an individual setting use the rule’s default action. The three available actions are:
Action | What Cloudflare does | Best used for |
| Normalizes request headers before selecting a cached variant, helping equivalent requests share a cached response. Applies header-specific rules to | The recommended starting point for negotiation headers where many request values map to a small set of responses. |
| Uses the request header’s raw bytes for cache matching, preserving casing, whitespace, order, and duplicate values. If the header appears on multiple lines, Cloudflare combines those lines in order using commas for cache matching. Passthrough leaves the outgoing header lines unchanged. Cloudflare can still rewrite Accept-Encoding when Respect Strong ETags is disabled. | Headers with a controlled set of values, where the exact value changes the response. |
| Does not store the response when the origin names that header in | Use for personalized, high-cardinality, or unexpected headers such as |
We recommend normalize as the default. For individual headers with personal or unbounded values, use bypass. Use passthrough when the exact value changes the response.
For example, passthrough preserves distinctions in casing, whitespace, ordering, and duplicate values, even when the origin treats them as equivalent. With Vary: X-View and passthrough, these three values produce separate cache keys:
X-View: compact,full
X-View: Compact,full
X-View: compact, full
Enough incidental variation can turn a reusable response into many one-off variants in your cache.
Regardless of the configured actions, Vary: * always bypasses cache. It means any aspect of the request, even information outside the HTTP message (like the client’s IP address), may affect which response the origin selects. Cloudflare therefore cannot reuse the response for a later request without contacting the origin.
How a response moves through cache
Let’s follow one of the /catalog requests from above through Cloudflare.
On the first request, Cloudflare has no stored Vary data for the resource, so the cache lookup misses. The matching Cache Rule can normalize configured fields before Cloudflare contacts the origin.
This can happen before Cloudflare knows whether the eventual response will contain Vary. The Cache Rule defines the permitted normalization; the response later determines whether those fields become part of the cached variant.
That ordering matters. If Cloudflare grouped several raw values under one normalized cache key, but the origin still received those raw values, the origin could produce different responses that the cache would later consider interchangeable. Forwarding the normalized value keeps origin selection aligned with cache matching.
The origin responds with:
Vary: Accept, Accept-Language
Cloudflare records those header names and stores the response as a cached variant. The header values, processed according to the Cache Rule, distinguish this variant from others for the same resource.
When another request for /catalog arrives, Cloudflare starts with the resource’s base cache key: generally the URL plus any other configured key fields. It then reads the stored Vary fields and applies the Cache Rule to those headers in the new request to identify the matching cached variant.
Suppose they normalize to:
Accept: text/html
Accept-Language: en,fr
Cloudflare uses those values to look up the matching cached variant directly. It does not compare the request against every stored variant one by one.
If a matching variant exists and is fresh, the request is a cache hit. If not, Cloudflare sends the request to the origin and may store the resulting response as another variant.
The origin response closes the loop. For each header named in Vary, Cloudflare uses the action configured for that header, or the rule’s default action if the header is not listed individually:
- If it does not contain
Vary, Cloudflare caches it normally. - If every named header resolves to normalize or passthrough, Cloudflare can store the response as a cached variant.
- If any named field uses bypass, Cloudflare does not store the response.
- If the response contains
Vary: *, Cloudflare does not store it.
This places an important responsibility on the origin. Every cacheable response that can differ based on request fields must return the appropriate Vary header consistently, including errors and fallback responses. If one response omits it, Cloudflare could cache that response without the variance needed to keep it isolated.
The cache keys in the diagram are conceptual. The later request assumes a fresh cached response.
Any purge targeting a cached resource covers all its Vary variants. Existing requirements for purging custom cache keys still apply.
Changing a Vary configuration does not automatically purge existing content. The new policy may produce different cache keys: requests can miss and refill under the new keys, while old entries remain until they expire or are purged.
Normalization keeps equivalent requests together
Remember the requests from above asking for English and French?
Accept-Language: en-US, fr;q=0.8
Accept-Language: fr;q=0.8, en-GB
Both requests prefer English, but passthrough would treat them as different variants. If the Cache Rule allows en, fr, and de, normalize reduces both to en,fr, allowing them to share a cached response.
To do this, Cloudflare lowercases values in Accept, Accept-Language, and Accept-Encoding, then sorts them by quality value, the highest first, with alphabetical ordering to break ties. The client’s ordering therefore does not affect the cache key. After sorting, Cloudflare strips parameters from entries with a nonzero quality value. It can also lose q=0 (“not acceptable”) when shortening language tags or filtering to the configured formats and languages. For example, en-US;q=0 can become en. Use passthrough for Accept or Accept-Language if the origin needs to see those exclusions.
You can also configure the rule to keep only specified media types or languages in Accept and Accept-Language. Regional language tags such as en-US reduce to their base language, en, unless the full tag is configured. This lets you align normalization with the formats and languages your origin actually serves.
To keep origin selection aligned with cache matching, Cloudflare forwards the normalized Accept and Accept-Language values to the origin. It also forwards normalized Accept-Encoding values when Respect Strong ETags is enabled. Other headers are normalized only for cache matching.
Configure Vary in Cache Rules
In the Cloudflare dashboard, go to Caching > Cache Rules, create or edit a rule, make the response eligible for cache, and add the Vary setting. Set the default behavior, then add the headers your origin is expected to name.
The same configuration is available through the Rulesets API in the http_request_cache_settings phase. The default setting chooses a fallback action for headers your origin names in Vary that you have not configured individually.
This example normalizes Accept and Accept-Language to a configured set of formats and languages. The default normalize action also applies to other headers named in Vary:
{
"rules": [
{
"ref": "vary_negotiated_content",
"description": "Cache bounded negotiated representations",
"expression": "(http.host eq \"example.com\" and http.request.uri.path eq \"/catalog\")",
"action": "set_cache_settings",
"action_parameters": {
"cache": true,
"vary": {
"default": {
"action": "normalize"
},
"headers": {
"accept": {
"action": "normalize",
"media_types": ["text/html", "application/json"]
},
"accept-language": {
"action": "normalize",
"languages": ["en", "fr", "de"]
}
}
}
}
}
]
}This is a complete request body for a PUT to the http_request_cache_settings phase entrypoint. A PUT replaces every rule in that entrypoint. If you already have Cache Rules, include them in the rules array or use the appropriate single-rule create or update operation instead.
If the origin serves one representation for each media type and language pair, there are six content combinations. That does not cap the cache at six keys. Preference order, missing headers, and values that normalize to empty can create more. Keep the supported set small and define the rule’s boundaries clearly. After rollout, test the same URL with different header values that should normalize to the same cached variant. Send the test requests from the same client, confirm they return the expected format and language, and inspect CF-Cache-Status. Look for hits once the cache is populated, and investigate persistent miss responses or unexpected bypass responses.
For limitations, additional examples, and how to set this in Terraform, see the Vary documentation.
Why not use a custom cache key?
At this point, an obvious question is, “why not add Accept and Accept-Language to a custom cache key?”
That works when those fields are always part of the resource’s identity. But a custom cache key adds the configured dimensions to every response covered by the rule, whether the origin used them or not.
Vary is response-driven, but cacheable responses under the same base key need a consistent set of Vary fields.
Use a custom cache key when a request property always defines the resource. Use Vary when the origin declares the same set of request fields across cacheable responses. Avoid placing the same header in both unless the duplication is deliberate and tested.
Use Vary in Cache Rules today!
Vary helps solve an obvious problem: one URL can have more than one correct response. But it hands a cache a harder problem, which request differences actually matter? The origin knows which responses it can serve. The cache needs to know which requests can reuse each response.
Vary in Cache Rules connects those two views. The origin identifies the request fields that may affect a response. You decide whether to normalize values, use passthrough for exact differences, or keep the response out of cache.
Vary was never too ugly to be useful. But configuring supported formats and languages manually may not suit every application. We’re evaluating whether ideas from the expired Availability Hints draft could reduce that work by letting origins describe the representations they serve directly.
Vary in Cache Rules is available today on Free, Pro, Business, and Enterprise plans through the Cloudflare dashboard, Rulesets API, and Terraform.



