5 min read

Caching and Invalidation

Adding a cache takes an afternoon. Knowing when its data is wrong is the actual work.


The story

An online shop caches product details in Redis for 24 hours. When a price changes, the code does the textbook thing: update the database, then delete the cache key so the next read fetches the new price.

During a sale, the marketing team drops a TV from ₹60,000 to ₹45,000. The database has the new price. The cache key was deleted. And yet for the rest of the day, product pages show ₹60,000, and conversion on the sale's headline item is zero.

Here's what happened, in the space of about 10 milliseconds:

The reader fetched the old price before the update and wrote it into the cache after the delete. Each step was correct on its own. The ordering wasn't.

Phil Karlton's line, "There are only two hard things in Computer Science: cache invalidation and naming things", gets quoted as a joke. This is why it isn't one.


The idea: caches are layered

You rarely have one cache. You have several, and each one can serve stale data.

Each layer closer to the user is faster and harder to invalidate. You can delete a Redis key instantly. You can't reach into a user's browser. So the closer to the user, the shorter the TTL or the more immutable the content (versioned filenames like app.3f9a2c.js, cached for a year).


How data gets into the cache

PatternGood forRisk
Cache-asideRead-heavy data. Only caches what's actually read.First read is slow, and the race in the story
Read-throughSame, with the cache library doing the DB readSame staleness issues
Write-throughData that's read right after it's writtenSlower writes, caching data nobody reads
Write-behindVery write-heavy counters, metricsData loss if the cache dies before the flush
Write-aroundWrite-once, rarely re-read data (logs, uploads)First read after write always misses

How data gets out: invalidation

Four tools, usually combined:

  1. TTL. Every key expires eventually. This is your safety net: whatever bug you have, staleness is bounded by the TTL. A 24-hour TTL on a price is a 24-hour bug.
  2. Delete on write. Update the DB, then delete the key. Cheap and usually right. The story's race is rare but real; it's fixed by a short TTL, a second delete a moment later ("delayed double delete"), or the next option.
  3. Event-driven invalidation. Read the database's change log (CDC, e.g. Debezium on the Postgres WAL) and delete the keys for every row that changed. It catches writes from any code path, including the admin script nobody remembered.
  4. Versioned keys. Put a version in the key (product:42:v17) and bump it on write. Old values are never read again and age out on their own.

The other failure: the stampede

A hot key, say the home page feed, expires. In the next 100 ms, 5,000 requests miss the cache at the same moment and all hit the database with the same expensive query. The database falls over, so the cache never gets refilled, so the next requests miss too.

Fixes:

  • Request coalescing / lock: the first miss takes a short lock and rebuilds the value, and everyone else waits for it or gets the stale value.
  • Stale-while-revalidate: serve the expired value while one background job refreshes it.
  • Jittered TTLs: TTL = 300s ± 10% so keys written together don't all expire together.
  • Early refresh: refresh hot keys shortly before they expire.

Let a hot key expire, then try the fixes:

Loading simulation…

Cheat sheet

QuestionAnswer
Default patternCache-aside + delete on write + a TTL as the safety net
How long a TTL?As long as the business can tolerate wrong data: seconds for prices and stock, hours for profile pictures
Must never be stale (balance, stock at checkout)Don't serve from cache. Read the DB for the decision.
Writes from many code pathsEvent-driven invalidation from CDC
Static assetsVersioned filenames + cache for a year
Hot keyCoalesce misses, stale-while-revalidate
Many keys expire togetherJitter the TTL
Cache fullLRU eviction by default. LFU if a few keys are always hot.
Cache goes downThe DB must survive the miss rate, or you need graceful degradation (SD 6)

Say it in the interview

"Cache-aside in Redis for product pages: read the cache, fill it on a miss, delete the key when the product changes, and keep a 5-minute TTL as the safety net for missed invalidations. Hot keys get request coalescing so an expiry can't stampede the database. Checkout reads price and stock from the database, never the cache."


Go deeper