advanced 2 min answer

A discussion platform puts a caching reverse proxy in front of its application. Anonymous traffic gets much faster; logged-in users see no improvement and occasionally see another user's content. What went wrong?

reverse-proxycachingcache-keyvarypersonalisationredditfailure-analysis
Show the full answer Hide the answer

The two findings are the same bug

The cache key is incomplete.

Logged-in responses vary by user — vote state, subscriptions, moderation privileges, preferences — but the cache key is derived from the URL alone. Two things follow, and they are the same defect seen from different sides:

  • Personalised responses cannot be cached correctly, so either they are excluded from caching (no improvement for logged-in users) or they are cached incorrectly (one user's response served to another).
  • Where the exclusion rule is imperfect — one endpoint, one header, one code path that forgets — the incorrect case leaks through, which is exactly the "occasionally" in the symptom.

Serving one user's personalised page to another is not a performance bug. It is a security incident, and it is the most severe failure mode a shared cache has.

Why it happens

  • Caching added at the infrastructure layer without the application declaring what varies. The proxy cannot know that a response depends on a session cookie unless something tells it.
  • Vary: Cookie used as the fix, which is technically correct and practically useless: every user has a distinct cookie, so the cache stores a copy per user and the hit rate is near zero. It converts a correctness bug into a memory problem.
  • A default caching rule with exceptions, rather than explicit opt-in. Any endpoint the exception list misses is cached wrongly, and the list is never complete.

The correct design

1. Make cacheability explicit and opt-in. Responses are uncacheable by default; an endpoint declares Cache-Control deliberately. The failure mode of forgetting is then a missed optimisation rather than a data leak.

2. Separate the cacheable shell from the personalised fragment. The thread, the post, the comment tree are identical for everyone and highly cacheable. Vote state, subscription status and preferences are small and per-user. Serve the shared shell from cache and fetch or inject the personalised fragment separately. This is what makes a discussion platform cacheable at all, and it is the highest-leverage change.

3. Cache key includes every dimension that varies the response — and the small set of legitimate dimensions should be an explicit, reviewed list: locale, device class, content-visibility flags. If the list is long, the response is not really cacheable and should be split.

4. Edge-side personalisation for the small variable parts, so one cached object serves everyone and the per-user portion is composed at the edge.

5. A test asserting that no authenticated response is ever cached with a key lacking user identity. This is a fitness function, and it is the only thing that reliably prevents recurrence, because the bug is reintroduced by ordinary changes rather than by carelessness.

The generalisable lesson

A cache is only correct if its key captures every input that varies its output. The most severe caching bugs are not staleness — they are key incompleteness, and they present as a security incident rather than as a performance problem. That asymmetry is why cacheability should always be opt-in.