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

Canonical URL: https://www.devobs.io/articles/qa-ge50-http-cache-variants-negotiation/
By: Sofia Reyes
Published: 2025-09-08T10:22:19.457Z
Updated: 2026-09-06T10:18:15.722Z
Section: Architecture

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](https://httpwg.org/specs/rfc9111.html). [Fastly’s cache-key guide](https://www.fastly.com/documentation/guides/full-site-delivery/caching/manipulating-the-cache-key/) 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](https://httpwg.org/specs/rfc9110.html), content coding via [`Accept-Encoding` in RFC 9110](https://httpwg.org/specs/rfc9110.html), and language via [`Accept-Language` in RFC 9110](https://httpwg.org/specs/rfc9110.html). 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) 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`](https://httpwg.org/specs/rfc9111.html) defines cache-key calculation with the `Vary` header field, and [Fastly’s cache-key guide](https://www.fastly.com/documentation/guides/full-site-delivery/caching/manipulating-the-cache-key/) 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:

```text
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](https://httpwg.org/specs/rfc9110.html). 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](https://httpwg.org/specs/rfc9110.html) 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.

## Source references

- <https://httpwg.org/specs/rfc9110.html>
- <https://httpwg.org/specs/rfc9111.html>
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation>
- <https://www.fastly.com/documentation/guides/full-site-delivery/caching/manipulating-the-cache-key/>
