Long before cache support existed for it, Vary was already the subject of complaint. One widely cited writeup calls it “the ugliest part of HTTP that we haven't yet improved,” a “horrible, kludgy mechanism” with “pretty abysmal interoperability” between intermediaries. That reputation does not make it dispensable: the same URL can legitimately map to more than one response, and a cache that ignores Vary may hand bytes to the wrong client. A cache that treats every raw header value as unique, on the other hand, can shatter into thousands of barely reusable entries.

The gap is that Vary names the request fields that may affect a response; it says nothing about which differences matter. Support for Vary is now available in Cache Rules on every plan, giving the operator control of that second question. The origin still declares which request headers may change the response; you choose how each one is handled — normalizing known negotiation headers, passing exact values through when precision is required, or skipping cache entirely when variation is too unpredictable.

The mechanics of Vary

A standard HTTP response header defined in RFC 9110, Vary tells intermediary caches such as Cloudflare which request fields the origin may have used to select the response. Sites lean on it to serve distinct languages, image formats, compression schemes, or regional content from a single URL.

GET /catalog HTTP/1.1
Host: example.com 
Accept: text/html

Suppose the origin answers with HTML and flags Accept as a field that may affect selection:

HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=3600
Vary: Accept

An API client could then hit the same URL with a different preference:

GET /catalog HTTP/1.1 
Host: example.com 
Accept: application/json

Here the correct answer is JSON. Because of Vary: Accept, a cache knows the URL alone cannot distinguish the two representations — the request's Accept value has to be considered. Skip the header and whichever response lands in cache first gets served to everybody: HTML in front of a JSON parser, or an API payload in front of a browser. Yet correctly avoiding that failure raises a further question — when two requests carry different header values, do they truly need different responses?

Correct, and completely cold

Vary describes which request fields may affect a response, but not what the response means. Consider an origin offering English, French, and German only. One client sends:

Accept-Language: en-US, fr;q=0.8

Another sends:

Accept-Language: fr;q=0.8, en-GB

Both prefer English, and the origin may well return the same English response to each. A cache comparing raw strings cannot make that leap: the two values differ in order and in language tags the origin does not differentiate, so they become separate variants even when the bodies are byte-identical.

This is the crux of the problem. Applications generate a small, finite set of representations from an enormous space of request values; the origin knows that thousands of language preferences collapse into three languages, and the cache generally does not. The damage multiplies across fields. Ten possible values on one field give ten variants; ten values across each of three fields give 1,000 combinations. Real headers go much further — User-Agent has huge cardinality, cookies can be unique per visitor, and preference headers vary in ordering, in formatting (spaces and tabs count), and in quality values.

The cache ends up perfectly correct and effectively never warm, with identical bodies spread across entries that see too little traffic to stay resident. They occupy capacity, evict each other, drag down hit ratios, and push more requests to origin servers. Eviction clears cold entries but cannot merge them merely because their payloads match.

The scale of the issue is measurable. 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 on 10, 23, or 47 fields.

Not all high-cardinality variation is accidental. CDNs and reverse proxies sometimes inject values such as geographic region to partition content on purpose — sound practice when the value set is controlled and every component agrees on what the values mean. Remove those constraints and the cache fragments into variants it may never reuse. Supporting Vary therefore meant holding both ends: enough variation to return the right response, not so much that incidental request differences gut cache efficiency.

Two decisions instead of one

Cloudflare customers already had routes for negotiated content: bypass cache and let the origin handle it, rebuild the negotiation logic in a custom cache key or another rule, write a Worker, or rely on narrower features such as Vary for images. Each remains useful, but they either abandon caching, duplicate application logic, require additional code, or cover only a slice of the problem.

Vary in Cache Rules splits the task in two: the origin uses Vary to name the request headers that may affect a response, and the Cache Rule decides how Cloudflare treats each header's value. The rule does not oblige every response to vary. Absent a Vary header from the origin, Cloudflare caches normally, though the rule can still rewrite Accept and Accept-Language before the request reaches the origin. When Vary is present, Cloudflare applies the configured action per named header, falling back to the rule's default action for headers without an individual setting. Three actions exist:

Action

What Cloudflare does

Best used for

normalize

Normalizes request headers before selecting a cached variant, helping equivalent requests share a cached response. Applies header-specific rules to Accept, Accept-Language, and Accept-Encoding. For other headers, it trims optional whitespace and combines repeated header lines in their original order, preserving casing and interior whitespace.

The recommended starting point for negotiation headers where many request values map to a small set of responses.

passthrough

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.

bypass

Does not store the response when the origin names that header in Vary. Existing cache entries are not removed, so purge them if they need to be cleared.

Use for personalized, high-cardinality, or unexpected headers such as Cookie or User-Agent.

Normalize is the recommended default. Reserve bypass for individual headers carrying personal or unbounded values, and choose passthrough when the exact value changes the response. Passthrough preserves casing, whitespace, ordering, and duplicate-value distinctions even when the origin considers the values equivalent — with Vary: X-View, these three produce separate cache keys:

X-View: compact,full

X-View: Compact,full

X-View: compact, full

Enough incidental variation of that kind turns one reusable response into many single-use variants. Independently of any configured actions, Vary: * always bypasses cache: it declares that any aspect of the request — even details outside the HTTP message, such as the client IP address — may influence origin selection, so no response can be reused without consulting the origin.

Following a request through the cache

Track one of the /catalog requests above through Cloudflare. On the first request there is no stored Vary data for the resource, so the lookup misses. The matching Cache Rule may normalize configured fields before Cloudflare contacts the origin — and this happens before Cloudflare knows whether the eventual response will carry Vary. The rule permits normalization; the response determines whether those fields end up part of the cached variant.

Ordering matters here. If Cloudflare folded several raw values under one normalized key while forwarding the raw values upstream, the origin could select different responses for what the cache would later treat as one variant. Forwarding the normalized value keeps origin selection and cache matching in agreement.

The origin replies with Vary: Accept, Accept-Language. Cloudflare records those header names and stores the response as one cached variant, distinguished from other variants for the same resource by those header values as processed under the Cache Rule. On a later request for /catalog, Cloudflare begins from the resource's base cache key — generally the URL plus any other configured key fields — reads the stored Vary fields, applies the Cache Rule to the new request's headers, and looks up the matching variant directly rather than scanning stored variants one at a time. If the values normalize to:

Accept: text/html

Accept-Language: en,fr

those are the values used for the lookup. A matching fresh variant is a hit; otherwise the request goes to the origin and the response may be stored as another variant.

The response closes the loop. For every header named in Vary, Cloudflare uses that header's configured action, or the rule's default when the header has no individual setting:

  • No Vary header: cache normally.
  • All named headers resolving to normalize or passthrough: store as a cached variant.
  • Any named field set to bypass: do not store.
  • Vary: *: do not store.

That puts real responsibility on the origin. Every cacheable response that can differ by request fields must return the correct Vary header consistently — including errors and fallback responses. A single response that omits it can be cached without the variance needed to keep it isolated.

Two operational notes. A purge against a cached resource covers all of its Vary variants, and the existing requirements for purging custom cache keys still apply. And changing a Vary configuration does not purge existing content on its own: the new policy may yield different cache keys, so requests miss and refill under the new keys while old entries persist until they expire or are purged.

How normalization collapses equivalent requests

Consider two requests that both prefer English but express it differently:

Accept-Language: en-US, fr;q=0.8

Accept-Language: fr;q=0.8, en-GB

Under passthrough, these count as distinct variants. If the Cache Rule permits en, fr, and de, normalize instead reduces both to en,fr, so the two requests can share one cached response.

Normalization in Accept, Accept-Language, and Accept-Encoding works by lowercasing values, then sorting by quality value with the highest first and alphabetical order as the tiebreaker. Because client ordering is discarded before the cache key is built, it cannot influence matching. Once sorted, parameters are stripped from entries whose quality value is nonzero. Where language tags are shortened or values are filtered to the configured formats and languages, a q=0 ("not acceptable") marker may be dropped — en-US;q=0 can become en, for instance. If the origin must still see those exclusions, use passthrough for Accept or Accept-Language.

A rule can also be constrained to retain only listed media types or languages in Accept and Accept-Language. Regional language tags such as en-US collapse to the base language, en, unless the full tag is explicitly configured. That alignment matters when the origin serves only a known set of formats and languages.

To keep origin selection and cache matching in agreement, Cloudflare forwards the normalized Accept and Accept-Language values upstream. Normalized Accept-Encoding is forwarded as well when Respect Strong ETags is enabled. Every other header is normalized for cache matching only.

Configuring the Vary setting

In the dashboard, open Caching > Cache Rules, create or edit a rule, make the response cache-eligible, then add the Vary setting: choose the default behavior and add the headers the origin is expected to name.

The Rulesets API exposes the same configuration in the http_request_cache_settings phase. The default setting supplies the fallback action for headers the origin names in Vary but that the rule does not configure individually.

The request body below normalizes Accept and Accept-Language against a configured set of formats and languages; the default normalize action covers any other header 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 body is a full request for a PUT to the http_request_cache_settings phase entrypoint, and a PUT replaces every rule at that entrypoint. Existing Cache Rules must therefore be included in the rules array, or a single-rule create or update operation used instead.

Six content combinations result if the origin serves one representation per media type and language pair — but that is not a ceiling of six cache keys. Vary preference order, absent headers, and values that normalize to empty all multiply the key space. Keep the supported set small and the rule boundaries explicit. After rollout, issue test requests from the same client against one URL with different header values that ought to normalize to the same variant, check that the expected format and language come back, and inspect CF-Cache-Status for hits once the cache is populated. Persistent misses or unexpected bypasses are worth investigating.

Limitations, further examples, and the Terraform setup are covered in the Vary documentation.

Vary or a custom cache key?

A natural alternative is adding Accept and Accept-Language to a custom cache key. That is correct when those fields always form part of a resource's identity — but a custom cache key attaches the configured dimensions to every response the rule covers, whether or not the origin actually consulted them.

Vary is response-driven, which means cacheable responses sharing a base key must agree on a consistent set of Vary fields. Choose a custom cache key when a request property always defines the resource; choose Vary when the origin declares the same request fields across its cacheable responses. Putting the same header in both is acceptable only when the overlap is deliberate and tested.

Availability

Vary in Cache Rules is available today on Free, Pro, Business, and Enterprise plans through the Cloudflare dashboard, the Rulesets API, and Terraform.

The mechanism addresses a familiar problem — one URL can have more than one correct response — at the cost of a harder one for the cache: which request differences are meaningful? The origin knows what it can serve; the cache must know which requests can reuse each response. Vary in Cache Rules joins the two, letting the origin identify the request fields that may affect a response while the operator decides between normalization, passthrough for exact differences, or keeping the response out of cache.

Manual configuration of supported formats and languages will not suit every application, and Cloudflare says it is evaluating whether ideas from the expired Availability Hints draft could reduce that work by having origins describe the representations they serve directly.