SOFTWARE / SYSTEMS / AIEngineering news. Technical depth.
Architecture / 4 MIN READ

How do we keep HTTP content negotiation from creating wrong or unbounded cache variants?

Treat content negotiation as cache-key design: vary only on normalized inputs that actually change the selected representation, and use explicit URLs for costly dimensions.

Treat content negotiation as cache-key design, not as a convenience feature. Decide which request properties truly change the selected representation, normalize those properties before they hit shared caches, and declare only those inputs in Vary. If a dimension is high-cardinality or user-specific, stop negotiating on it and make it explicit in the URL or endpoint design instead. HTTP caches use Vary to distinguish negotiated responses. Fastly’s cache-key guide explains how excessive key inputs reduce hit ratio and complicate purging.

Which request dimensions should actually create variants?

Only vary on dimensions that change the bytes or metadata of the representation you serve. In practice, common dimensions are media type via Accept in RFC 9110, content coding via Accept-Encoding in RFC 9110, and language via Accept-Language in RFC 9110. HTTP semantics defines these among the standard negotiation inputs, but you only need to vary on the ones that actually affect your selected representation. MDN’s content negotiation reference likewise describes these request headers as hints that the server may use, with Vary indicating which ones actually mattered.

Do not feed raw browser header values directly into shared-cache variance unless you truly support that granularity. Raw Accept-Language is the classic trap: clients often send long preference lists, while applications usually support only a small locale set. MDN’s content negotiation reference notes both the verbosity of these headers and the fingerprinting risk from language preferences. Normalize at the edge or origin into buckets like en and fr, then vary on that normalized result. Likewise, avoid User-Agent unless you are intentionally creating device- or browser-specific bytes.

How should Vary and cache keys work together?

Shared caches calculate negotiated variants using Vary, while CDNs may also let you customize the cache key. The safe pattern is: normalize first, then vary precisely. RFC 9111 defines cache-key calculation with the Vary header field, and Fastly’s cache-key guide warns that adding too much information can reduce hit ratio and make purging harder.

A concrete example for GET /profile:

  • Formats: HTML or JSON
  • Languages: English or French
  • Encodings: a deployment that serves Brotli, gzip, or unencoded bytes

Normalize requests before caching:

  • Accept becomes html or json
  • Accept-Language becomes en or fr
  • Accept-Encoding becomes br, gzip, or the unencoded variant (identity on the selection side) based on what the client accepts and what the origin serves

Then emit response metadata such as:

Content-Type: text/html; charset=utf-8
Content-Language: en
Content-Encoding: br
Vary: Accept, Accept-Language, Accept-Encoding

For the unencoded variant, omit Content-Encoding rather than sending Content-Encoding: identity, consistent with the content-coding model in RFC 9110. That keeps the variant space bounded by the buckets you actually support, rather than by every raw header permutation. If you support 2 formats, 2 languages, and 2 compressed encodings for a client population that accepts one of those encodings, that is 8 cache objects. If you also need to support the unencoded form, count that as a separate variant. If clients need stable, linkable locales or formats, prefer /en/profile or /profile.json; negotiation is best kept for dimensions like compression that users do not manually navigate.

What breaks validators on negotiated responses?

Validators must match the selected representation, including content coding when the bytes differ. RFC 9110’s validator sections discuss validators, strong versus weak comparison, and include an example of entity tags varying on content-negotiated resources. If you serve both Brotli and gzip bytes, do not reuse one strong ETag for both unless the representation data is actually identical byte-for-byte. A weak ETag can express semantic sameness across variants, but strong validation is about exact bytes.

Last-Modified is coarser and can still be useful, but it cannot distinguish two simultaneously valid encoded byte streams as precisely as separate ETag values. When validating negotiated responses, make sure the validator you use corresponds to the selected variant.

When should we stop negotiating and use explicit URLs instead?

Use explicit URLs when the dimension is user-visible, high-cardinality, or operationally expensive to purge and debug. Locale, file format, version, and major device classes often become easier to reason about as path or hostname choices. Keep negotiation for low-cardinality transport concerns like compression, and for a small number of representation types when one canonical resource really does have a few interchangeable forms.

Decision checklist:

  • Does this header change bytes, or only preference ordering?
  • Can you normalize it into a small bucket set?
  • Can your CDN purge all variants predictably?
  • Will humans need stable URLs for linking, SEO, or support?
  • Do your ETag values match each encoded variant?

Q: Should we put raw Accept-Language into the cache key?
No. Normalize it to supported locales first, or use explicit locale URLs.

Q: Is Vary: User-Agent a good fallback?
Usually no. It creates large, fragile variant sets unless you have a tightly controlled normalization layer.

Next step: pick one negotiated endpoint, enumerate the exact variant dimensions it really needs, and compare that list with the headers currently reaching your CDN.

Reviewed: 2026-09-06.

SOURCES & REVIEW

Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.

Read our editorial approach ↗