You run a CDN in front of an API and the hit rate is poor because responses declare several request headers in the HTTP Vary header. How would you decide which headers belong in the cache key, and how would you normalize them?
answer
- Key = product of cardinalities
- Delete axes that change nothing
- Normalize to what the origin can emit
- Coarse+stable → put it in the URL
- Per-user → private/no-store, not clever keys
basics
~20 sKeep only headers that actually change the bytes, and collapse each to the smallest set of values the origin can really produce. Anything high-cardinality gets normalized to a derived low-cardinality header, or the variation moves into the URL instead.
solid answer
~50 sI start by measuring: for a few hot URLs, count distinct stored variants and list the headers in `Vary`. Hit rate degrades roughly with the product of the cardinalities, so the biggest offender is usually one high-cardinality header rather than the count of headers. Then each header faces three questions. Does the response body or headers genuinely differ along this axis? If not, remove it. If yes, how many distinct outputs can the origin actually produce? Normalize the header to exactly that many tokens at the edge — `Accept-Encoding` collapsed to `gzip`/`br`/none, a cookie reduced to `X-Auth-State: anon|auth`, `User-Agent` bucketed to device class. If the axis is coarse, stable and interesting to observe, lift it into the URL instead — `?locale=de` — because that makes it visible in logs, dashboards and purges. What is left is per-user content, which should not be shared at all: `Cache-Control: private` or `no-store`, or split into a cacheable shell plus an uncacheable fragment.
go deeper
Keep it simple: only vary on headers that really change the response, and prefer few possible values.
Explain the cardinality product, give concrete normalizations for Accept-Encoding and Accept-Language, and know that per-user content should not be shared.
Measure variants per URL, find the dominant offender, implement edge normalization, and treat client-controlled key inputs as a flooding risk.
Own the policy: which axes exist, where each lives (URL vs key vs not cached), the personalization fidelity you are trading away, and the tests and metrics that keep it from drifting.
## Frame it as cache-key design The cache key is a product space. Primary key (URL) times every header named in `Vary`, each contributing its cardinality. Two encodings times five languages times a device class of three is thirty objects per URL — thirty misses to fill, thirty entries competing for storage, and thirty chances that the object a given user needs has been evicted. Hit rate does not fall linearly with the number of headers; it falls with the product of their cardinalities. So the work is not 'have fewer Vary headers', it is 'make each axis as small as it can be while staying correct'. ## Step 1 — measure before changing anything Pick the hottest URLs and pull, per URL: the `Vary` value, the number of distinct stored variants, hit rate, and origin request rate. Most CDNs expose variant counts or at least a cache-status header you can bucket. The usual finding is one dominant offender — `Cookie`, `User-Agent`, or a raw unnormalized `Accept-Encoding` — producing thousands of variants while the other axes contribute two or three each. ## Step 2 — classify each header For every name in `Vary`, ask in order: 1. **Does the response actually differ along this axis?** Framework middleware adds headers reflexively — session middleware appending `Cookie`, CORS middleware appending `Origin` to responses with a constant wildcard. If the bytes never change, delete the axis. This is usually the single biggest win and costs nothing. 2. **How many distinct outputs can the origin actually produce?** This is the true cardinality. If the origin only ever emits identity or gzip, then `Accept-Encoding` has cardinality two no matter how many distinct strings clients send. 3. **Can the axis move into the URL?** A path or query parameter is a better place for coarse, stable variation: `/v1/pricing?country=de`. It is visible in access logs, purgeable per value, shareable as a link, and testable. `Vary` is for axes that must stay invisible to the client — compression, CORS reflection. ## Step 3 — normalize at the edge Normalization means rewriting the request header, before the key is computed, to a canonical token drawn from that true cardinality: - `Accept-Encoding`: parse, ignore q-values and ordering, emit `br` if supported and produced, else `gzip`, else empty. - `Cookie`: never key on it raw. Derive one bit or one small enum — presence of a session cookie, an experiment bucket — into a dedicated header such as `X-Auth-State` and vary on that. - `User-Agent`: never key on it raw either; bucket to `mobile|tablet|desktop`, or better, stop varying and serve responsive content. - `Accept-Language`: map to the languages you actually publish, with a fallback, so `de-AT,de;q=0.9,en;q=0.8` becomes `de`. - `Origin`: leave as-is when reflecting from a bounded allowlist — cardinality equals the allowlist size, which is small — but reject non-allowlisted origins early so they collapse to one variant. Normalization has to be **deterministic and total**: every possible input maps to one of the canonical tokens, including missing and malformed values, otherwise attacker-controlled or exotic headers reintroduce unbounded cardinality. A header you key on that a client can set freely is also a cache-flooding vector: someone can push a hot object out of storage by cycling values. ## Step 4 — handle what cannot be normalized Whatever is left — genuinely per-user representations — should not be in a shared cache at all. Options in preference order: mark it `private` or `no-store`; split the page into a highly cacheable shell plus a small uncacheable personalized fragment fetched separately; or move personalization to the client. Trying to make per-user content shareable through clever keys is where cross-user data leaks come from. ## Step 5 — make it a contract, not a habit Centralize the policy in the edge configuration rather than trusting each service's headers, and add tests: assert that responses with a reflected `Access-Control-Allow-Origin` include `Origin` in `Vary`, that no production response carries `Vary: Cookie` or `Vary: User-Agent` unless explicitly waived, and that compressible content carries `Vary: Accept-Encoding`. Then watch variant count per URL as a first-class metric alongside hit rate — hit rate alone hides key explosion, because the CDN happily reports high hit rates on the subset of objects that do get reused while the origin still carries the load. ## The tradeoff to state out loud Every normalization is a deliberate loss of fidelity: bucketing `Accept-Language` means some users get a coarser language match; collapsing `User-Agent` means you stop serving device-specific markup. That is the trade — you are buying origin protection and tail latency with a small amount of personalization. Decide it explicitly per endpoint rather than letting middleware defaults decide it for you.
- How can a cache key become an availability risk rather than just a performance one?If a client-controlled header is part of the key without normalization, an attacker can cycle values to create unlimited distinct variants, evicting hot objects and forcing all traffic to the origin. Normalizing every keyed header to a small closed set of tokens removes the lever, and per-URL variant-count alerts detect it if it happens.
- Why prefer a query parameter over a Vary axis for something like locale?Because the variation becomes part of the primary key: it is visible in access logs, individually purgeable, linkable and easy to test, and it cannot be lost by an intermediary that mishandles Vary. The cost is a uglier URL and the need to redirect or default when the parameter is absent.
Sorting a warehouse: shelving by exact customer name gives one box per shelf; shelving by size and colour gives a few well-stocked shelves everyone can pick from.
saying these in an interview costs you the question
- Optimizing the number of Vary headers instead of the cardinality of each axis
- Keying on a raw client-controlled header with no normalization
- Judging cache health by hit rate alone without looking at variants per URL
- Trying to share per-user responses safely through a cleverer cache key
- Leaving framework-default Vary headers unaudited across services