← ~/blog

Caching Is Easy, Invalidation Is a Career

 /  systems  /  300 words

Adding a cache takes an afternoon and makes a graph go down, which is why everyone does it. Then the cache starts serving lies, and you discover the afternoon was a down payment.

A cache entry is a copy of some truth, and every copy comes with an unwritten contract about how stale it may get. Most caching bugs are the contract being implicit. Somebody caches a permissions lookup for five minutes because five minutes felt reasonable, and now revoking access takes effect eventually, a word that does not appear in the security policy.

Your realistic options, in ascending order of ambition. TTL only: simplest, staleness bounded by the TTL, correct choice more often than pride admits, just pick the number consciously per data type instead of copying 300 from the last cache. Explicit invalidation on write: precise when it works, but now every code path that mutates the data must know every cache that copies it, and the one path that forgets, a batch job, a manual SQL fix, an admin tool built in 2022, becomes a permanent staleness generator. Event driven invalidation off change streams catches those, at the price of new infrastructure and a propagation delay. Versioned keys sidestep deletion entirely, write new data under a new key and update the pointer, which is lovely until you meet the eviction pressure of storing every version.

Two failure modes deserve their own alarms. Stampedes, where a popular key expires and a thousand requests simultaneously go rebuild it against your poor database, fixed with request coalescing or jittered TTLs. And negative caching, where you cache "not found" and then the thing gets created and stays invisible until the TTL says otherwise.

Name the staleness contract out loud in review. Everything else about caching is arithmetic. This part is promises.