Vary Against Your CDN: How Content Negotiation Quietly Destroys the Hit Rate
A primary CDN vendor doc or RFC that explicitly quantifies how many cache entries a given Vary line creates, to allow comparison across different usage of Vary by a CDN engineer.

What is in this piece
# Vary Against Your CDN: How Content Negotiation Quietly Destroys the Hit Rate Content negotiation can shatter cache performance, with Vary adding header values to the cache key and multiplying variants. The Accept-Encoding header to control compression types seems to be the only safe example, with normalization recommended.
Vary as Cache-Key Multiplication
CDN caching is about keys that are the same when all the information in those boxes is the same. A cache key is basically like a fingerprint for a specific requested asset... The Vary header is part of that. The Vary header specifies the exact thing that the cache needs to consider equivalent or acceptable for this fingerprint. So, in other words, if user agents that are certain ways or send different Accept-Language headers or send slightly different Cookie headers, those can have cacheable assets.
So, in other words, Vary: Accept-Encoding will cache a single URL differently for gzip and Brotli. But Vary: *, meaning the response varies on factors beyond what can be encoded in the request headers, will stop a well-behaved CDN from serving the response from cache at all.
Only vary on headers that have a known, limited set of values. That lets the CDN keep the cache key's combinatorial complexity low.
It is often observed that once you have more than a few vary headers in the response, the cache hit ratio becomes unreliable. The combinatorial complexity can be too high to get useful cache hits. You care about things that impact the variance across those varying headers.
The One Safe Example: Accept-Encoding
Accept-Encoding is safe as a Vary value, because the number of compression types is low, and so many clients use a consistent negotiation order, that a normalized response is likely to represent the vast majority of clients. That just means you only need to have different headers for some values of Accept-Encoding.
Accept-Encoding is often necessary, because the same URL should serve different compressed representations, like gzip and Brotli, for clients that support them. Reaching normalization on Accept-Encoding becomes the model for saying where to vary on headers.
The Bad Actors: Language, User-Agent, Cookie
User-Agent and Accept-Language cause fragmentation, because there are many values, and even more permutations between them. That performs the opposite of changing the cache key, it multiplies the cache keys, so the CDN has to manage many variants.
User-Agent is effectively uncacheable at the shared-cache layer. When you Vary: User-Agent, you create a separate cache entry for each distinct User-Agent string.
That leads to high-cardinality values that produce a low hit ratio. A near-zero cache hit ratio again means more origin requests, more bandwidth, and higher operational costs.
Storing by Accept-Language can be a similar problem, especially in the context of international sites. Accept-Language: en-US, fr, fr-FR; q=0.8 are equivalent in some third-party services, for normalization, but can produce two to three separate cache entries raw.
If the cardinality of the header values was still too high even after normalization, as a last resort, set Vary: * to ensure the response is not cached. That ultimately puts the cache decision in the hands of the origin server after the CDN determines that Accept-Language is the key differentiator.
Normalizing at the Edge
CDN vendors offer features that can normalize headers to reduce the cardinality of a Vary value. Custom Caching Rules at Cloudflare allows normalizing Accept-Language values.
That means the CDN only needs to cache one copy of the asset for a set of semantically equivalent Accept-Language values. The asset would be cached once in the shared cache for Accept-Language: en-US, fr, fr-FR and Accept-Language: fr, fr-FR, en-FR.
Operational Rule
To avoid depressing the cache hit ratio, only Vary on a limited set and normalize at the edge. Accept-Encoding is the well-behaved, and often Accept-Language can be the misbehaved, header in comparison.
If you cannot get enough normalization, to a manageable number of cache keys, via Vary or custom rules, consider placing the value in a Cookie, instead. Just remember that this has other costs, like making the response uncacheable.
What to Do Instead of Raw Header Negotiation
After normalization, move decision-making about language, user-agent, or format out of headers.
If there is a static, known set of Accept-Language values, represent each by a separate URL under a /language/ bucket. Consider whether the application really ever needs more than a dozen or so, localized representations of a view, like an article, that is the same information in a majority of cases. A real-world design pattern of moving localized or user-agent-specific representations to subresources.
Cache control becomes easier to reason about. The caches just need to store the English and French home.html, and do not need to store a million home.html variants.
Conclusions
There are many more options than skilled engineers believe, between hitting origin for every variation, and incurring excessive cache key cardinality and fragmentation from complex, per-variant Vary lines.
Many different areas of CDN configuration choices determine hit ratios. But Vary is where fragmentation, low hit rates, and high origin traffic often start. We recommend normalizing Accept-Encoding at the edge, and anything other or additional can best be done via dedicated language buckets or sub-resources.
- 01Web Platform
CORS Preflight Requests Matter - The Browser Blocks Prods BUT NOT Your Local
The quick test looks great: your browser client sends the request, the dev server responds, and your UI updates happily. Delighted, you release into production. Minutes later, users are…
- 02Web Platform
The Native <dialog> Element Replaced Your Modal Library — Here Is What It Still Will Not Do
The <dialog> element includes a lot of modal behavior out of the box.

