Creuto is now an OpenAI Select Partner Read More

Custom Software Development

HTTP Vary header: cache personalised pages without leaks

The HTTP Vary header is where personalised pages break. Why Vary on Cookie kills your hit rate, how normalisation saves it, and the leak to audit for.

HTTP Vary header: cache personalised pages without leaks

The HTTP Vary header is the only standard way to tell a cache that one URL has more than one correct answer. It is also the fastest way to make a cache useless. Vary on Cookie and your hit rate collapses to roughly zero. Omit it from a single error page and a cache can hand one customer another customer's content.

Cloudflare shipped Vary support in Cache Rules on 22 September 2026 and opened the post by quoting an earlier description of Vary as "the ugliest part of HTTP that we haven't yet improved", a "horrible, kludgy mechanism" with "pretty abysmal interoperability" across intermediaries. That is a strange thing for a CDN to say while announcing support for it. It is also the most honest framing anyone has given the header.

What the HTTP Vary header actually tells a cache

Vary is a response header that names the request fields which may have influenced the response. MDN describes it as the parts of the request message, aside from the method and URL, that shaped the response — and adds the rule most origins break: the same Vary value should be used on all responses for a given URL, including 304 Not Modified responses and the default response.

The canonical example is content negotiation. A browser sends Accept: text/html, an API client sends Accept: application/json, and the origin answers the same /catalog URL two different ways. Without Vary: Accept, whichever response enters the cache first is served to both. If HTML wins, the API client's JSON parser fails. If JSON wins, a browser gets an API response.

The normative behaviour lives in RFC 9111 section 4.1. A cache must not use a stored response without revalidation unless every presented request header field nominated by Vary matches the original request. Matching is generous in defined ways — whitespace where the syntax allows it, combining repeated field lines, normalisation that is known to preserve semantics — but it is otherwise exact. Two rules bite in practice: a field absent from one request can only match another request where it is also absent, and a stored response whose Vary value contains * always fails to match.

So Vary tells the cache which fields may matter. It never tells the cache which differences actually matter. That gap is the entire problem, and it is worth reading section 4.1 directly rather than through a summary — the same habit that paid off when we read the HTTP QUERY method RFC instead of the coverage of it.

Why varying on Cookie destroys the cache

Cloudflare's post puts numbers on the gap. Ten possible values in one field create ten variants. Ten values across three fields can create 1,000 combinations. Cookies can be unique to individual visitors, User-Agent values are effectively unbounded, and preference headers differ in ordering, formatting and quality values — spaces and tabs included.

The result is a cache that is perfectly correct and almost permanently cold. Identical responses scatter across entries that never get enough traffic to stay hot, consume capacity, evict one another, and push more requests back to the origin. Eviction removes cold entries; it cannot merge two entries just because their bytes are identical.

This is not a theoretical failure. Cloudflare cites an analysis of more than 120 million responses from nearly 50,000 popular sites that found almost 3,000 sites varying on four or more fields, with some varying on 10, 23 or even 47.

Cloudflare's guidance for a header like Cookie is to bypass cache entirely. Its bypass action means the response is not stored when the origin names that header in Vary — and existing entries are not removed, so you must purge them if they need to be cleared. If Vary: Cookie is on your HTML responses today, you do not have a cache in front of your pages. You have a proxy.

Accept-Encoding and the normalisation that saves your hit rate

Vary on Accept-Encoding is the case where the header earns its keep, because the variation is small and bounded: a handful of compression schemes, not a per-visitor identifier. Cloudflare's Cache Rules give you three actions per header, and the choice matters more than the list suggests.

ActionWhat the cache doesUse it for
normalizeNormalises the request header before selecting a variant, so equivalent requests share one entryNegotiation headers where many values map to few responses — the recommended default
passthroughMatches on the raw bytes, preserving casing, whitespace, order and duplicatesHeaders with a controlled set of values where the exact value changes the response
bypassDoes not store the response at all when the origin names that headerPersonalised, high-cardinality or unexpected headers such as Cookie or User-Agent

Normalisation is what collapses the combinatorics. Cloudflare lowercases values in Accept, Accept-Language and Accept-Encoding, sorts them by quality value with the highest first and alphabetical ordering to break ties, then strips parameters from entries with a nonzero quality value. So Accept-Language: en-US, fr;q=0.8 and Accept-Language: fr;q=0.8, en-GB both reduce to en,fr and share one cached response, where passthrough would have stored two.

There is a real cost to that convenience, and Cloudflare names it. Normalisation can lose q=0 — "not acceptable" — when shortening language tags or filtering to configured formats, so en-US;q=0 can become en. If your origin depends on seeing exclusions, use passthrough for that header instead.

Passthrough has the opposite failure. With Vary: X-View and passthrough, the values compact,full, Compact,full and compact, full produce three separate cache keys even though the origin treats them as one. Enough incidental variation turns one reusable response into a pile of one-off variants.

One more absolute: Vary: * always bypasses cache. MDN notes the wildcard implies the response is uncacheable, and Cloudflare will not store it regardless of the actions you configure, because anything outside the HTTP message — the client's IP address, for instance — may have selected the response.

The leak is one response without Vary

Here is the failure mode that costs a company a customer rather than a hit rate, and it is not caused by varying on the wrong thing. It is caused by inconsistency.

Cloudflare states the responsibility plainly: 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, the cache can store that response without the variance needed to keep it isolated — and then serve it to a request that should have received something else.

RFC 9111 describes the same hazard from the cache's side. Some resources mistakenly omit Vary from their default response, which has the effect of choosing that response for later requests even when more suitable ones are stored. The spec's mitigation is defensive, not curative: where multiple stored responses exist and one omits Vary, the cache should prefer the most recent stored response that has a valid Vary field value.

Translate that into the code you own. The personalised page probably sets Vary correctly. The 500 page rendered by your error boundary probably does not. Neither does the 304, or the maintenance page your load balancer serves, or the response your framework returns when a session lookup fails. Each of those is a response that can enter the cache with no variance attached and then be served to everybody.

Audit for consistency, not for correctness on the happy path. The same discipline applies to any edge logic that rewrites responses — a habit worth carrying over from the Cloudflare Workers production issues worth learning early, where the bugs that survive testing are the ones on paths nobody drove.

What to move to the edge instead

The durable fix is not a cleverer Vary configuration. It is removing the reason to vary at all.

Split the response. Cache the document that is identical for every visitor, and fetch the personalised fragment separately — the cart count, the greeting, the pricing band. The cached document keeps a high hit rate; the fragment is small, uncached and fast. This is the oldest advice in CDN engineering and it remains the only one that scales.

Where the difference is genuinely part of the resource's identity, use a custom cache key rather than Vary. Cloudflare draws the line clearly: a custom cache key adds the configured dimensions to every response covered by the rule, whether the origin used them or not, so use it when a request property always defines the resource, and use Vary when the origin declares the same set of fields across cacheable responses. Putting the same header in both is a trap unless the duplication is deliberate and tested.

Two operational notes people discover the hard way. A purge targeting a cached resource covers all of its Vary variants. But changing a Vary configuration does not purge anything — the new policy produces different cache keys, so requests miss and refill under the new keys while the old entries sit there until they expire or you purge them.

The Next.js guidance that follows is ours, not Cloudflare's, and it comes from the web app development work we do rather than from a spec. Keep session-dependent rendering out of any route you intend to cache at the edge, and be suspicious of middleware that touches cookies on every response — it is the commonest way Vary: Cookie ends up on pages nobody meant to personalise. Where a page needs one personalised element, we render the shell statically and hydrate that element from a separate uncached endpoint. It is less elegant than a personalised document and roughly an order of magnitude better for cache behaviour.

How to test it before you trust it

Cloudflare's own test procedure is the right one and takes ten minutes. After rollout, request the same URL with different header values that should normalise to the same variant, send them from the same client, confirm each returns the expected format and language, and inspect CF-Cache-Status. Look for hits once the cache is populated, and investigate persistent misses or unexpected bypasses.

Then run the inverse test, which is the one that catches leaks: request the URL as two different logged-in users and confirm neither ever receives the other's body. Include the error paths. Force a 500, force a 304, and check the Vary header on both.

Keep the supported set small. Cloudflare's example configures Accept to two media types and Accept-Language to three languages, and then warns that six content combinations do not cap the cache at six keys, because preference order, missing headers and values that normalise to empty can create more. Vary in Cache Rules is available on Free, Pro, Business and Enterprise plans through the dashboard, the Rulesets API in the http_request_cache_settings phase, and Terraform.

If you are about to enable this, start by listing every cacheable response your origin can emit and checking which ones carry a Vary header. That list is usually shorter than the number of responses you actually serve, and the difference between the two is your exposure. Performance work at the edge tends to pay off in the same places as the rendering changes we covered in out-of-order HTML streaming in Chrome 150 — the fastest response is still the one you did not have to generate.

Frequently asked questions

The HTTP Vary header tells caches which request fields, other than the method and URL, may change the response. A cache must not reuse a stored response unless every field named in Vary matches the original request, so Vary is what lets one URL hold several correct representations.

Usually because you are varying on a high-cardinality header. Ten values in one field create ten variants, and ten values across three fields can create a thousand combinations. Cookie and User-Agent values are near-unique per visitor, so each request creates an entry that is never reused.

Not the personalised document itself, reliably. The workable pattern is to cache the shell that every visitor receives identically and fetch personalised fragments from a separate uncached endpoint. Varying the whole page on Cookie is correct but produces a cache that stores entries it will never serve twice.

Yes, when compression differs by client, because the value set is small and bounded. Normalising it keeps equivalent requests on one cache entry. Cloudflare lowercases and sorts Accept, Accept-Language and Accept-Encoding values by quality value so client ordering does not fragment the cache.

A custom cache key adds your chosen dimensions to every response covered by the rule, whether the origin used them or not. Vary is response-driven: the origin declares the fields. Use a custom key when a request property always defines the resource, and Vary when the origin declares it.

Yes. A stored response whose Vary value contains the wildcard always fails to match under RFC 9111, and MDN notes that the wildcard implies the response is uncacheable. Cloudflare does not store a response carrying Vary: * regardless of the actions configured in the Cache Rule.

Written by

Akash Mohapatra

Akash Mohapatra

Co Founder & Director

23 Sep 2026

·

10 min read

Share

LET'S CONNECT

Connect with Creuto!

Ready to take the first step towards unlocking opportunities, realizing goals, and embracing innovation? We're here and eager to connect.

We don't just aim to fit in – we strive to stand out. Experience the perfect blend of innovation, excellence, and trust that makes us truly unforgettable. Discover the difference with Creuto.

© 2026 Creuto All Rights Reserved