Skip to content
stateless.co · Engineering notes from the request/response layer
statelessThe engineering desk

A publication about the machinery under everyday software: the contracts between services, the queries behind a page, and the failures that only show up in production.

02Web Platform

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.

Vary Against Your CDN: How Content Negotiation Quietly Destroys the Hit Rate
Photo: Samuel Zeller samuelzeller · CC0 · Wikimedia Commons
What is in this piece
  1. Vary as Cache-Key Multiplication
  2. The One Safe Example: Accept-Encoding
  3. The Bad Actors: Language, User-Agent, Cookie
  4. Normalizing at the Edge
  5. Operational Rule
  6. What to Do Instead of Raw Header Negotiation
  7. Conclusions

# 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.