Caching is a naming problem
Every cache bug I have shipped was really a cache key bug. Here is the checklist I now run before adding one.
A cache is a dictionary with a deadline. The deadline gets all the attention —
Cache-Control, max-age, stale-while-revalidate — but
the dictionary half is where things go wrong. If two requests that should see
different responses hash to the same key, no TTL will save you.
Start from the response, not the URL
The key has to name everything the response depends on. The path is the
obvious part; the Vary headers, the tenant, the feature flags in
play and the API version are the parts that get forgotten. Write them down
first, then build the key from that list:
const key = [
req.method,
url.pathname + url.search,
tenant.id,
vary.map((h) => req.headers.get(h) ?? '').join('|'),
].join(' ')
const hit = await cache.match(key)
if (hit && ageOf(hit) < maxAge) return hit
return fetchOrigin(req)
If you cannot explain in one sentence why two requests share a key, they should not share a key.
The checklist
- Does the key include everything in
Vary? - Does it include the identity of whoever is asking, if the answer differs?
- Is the key stable across deploys, or does a build hash sneak into it?
- Can you list the keys for one user and purge them all?
One more thing I do on every project now: a debug palette that shows the computed key for the current page. Press ⌘ K in the staging build and it lists the key, the age and the TTL. It has caught more bugs than any test I have written for caching.