Loading...
Loading...
Cache update strategies — when to use each and their trade-offs
The last topic ended with a price cut from $49 to $39 at noon sitting in four places at once, browser, edge, Redis, and database buffers, with four TTLs disagreeing about how stale is acceptable. Until someone tells the cache, every visitor sees $49, clicks buy, and gets charged $39, or worse, sees $39 and gets charged $49. A stale cache isn’t a performance problem. It’s a lying problem.
Keeping copies honest is genuinely one of the two hardest problems in computing (the other is naming things). Four strategies exist, and each picks a different point on the triangle of freshness, speed, and simplicity.
Computer scientist Phil Karlton named cache invalidation one of the two hard things in computing, alongside naming things. This topic is why he said it.
The default everywhere, where the application checks the cache first and fills it on a miss. Ask the cache; on a miss, ask the database and scribble the answer down. On writes, update the database and rip the sticky note up so the next read fetches fresh.
// Read
answer = remember(key) or fetch-and-remember(key)
// Write
save(key, value); forget(key)
Wins
Costs
Every write goes to the cache and the database before returning. Reads never lie, but every write pays double.
Wins
Costs
Write to the cache, answer instantly, flush to the database later in batches. Blazing writes, with a window where the truth exists in exactly one place that could catch fire.
Wins
Costs
Watch expiry dates and quietly refetch hot keys just before they die. Nobody ever pays the miss tax, as long as your predictions about “hot” are right.
if expiring_soon(key) and is_popular(key):
quietly refetch in background
serve from memory either way
Wins
Costs
| Strategy | Grab it when… | It costs you… |
|---|---|---|
| Lazy (cache-aside) | Starting out, unpredictable traffic | Occasional stale reads, miss storms |
| Write-through | Stale is unacceptable | Slower writes, fatter cache |
| Write-behind | Firehose writes, losable data | A crash window with real loss risk |
| Refresh-ahead | A few famous hot keys | Prediction machinery |
Every strategy above gets safer with one backstop: a TTL, a time-to-live expiry timestamp after which the entry dies on its own. Even if your invalidation logic has a bug, the lie expires on its own. Three flavors cover the field, plain expiry for everything, active deletion on write for the important stuff, and pub/sub broadcasts, a channel every cache subscribes to for invalidation news, when dozens of caches must hear the news at once.
Lies die on schedule. Blunt, bulletproof, slightly stale by design.
Kill the note the moment truth changes. Precise, but you must catch every write path.
Publish “price changed” and let every cache invalidate itself. For fleets, not singles.
Before wiring deletion into every write path, teams set a 60-second TTL and trust expiry alone, because no write path needs changing. It fails on arithmetic. With 1,000 writes per day the expected stale exposure is seconds per incident, tolerable. With 100 writes per second to one hot key, the entry is stale more often than fresh, and every reader in between pays the lie. Delete-on-write plus a TTL backstop costs one delete per write path and bounds the lie to the race window; expiry alone bounds it to the full 60 seconds, which is why hot keys graduate to write-through instead of longer TTLs.
Cache-aside invalidation has a classic interleaving that no TTL fully hides. Reader A misses, starts a slow database read of the old price ($49). Writer B updates the price to $39 and deletes the cache key. Reader A's stale $49 read finally returns and populates the cache, resurrecting the lie after the truth already won. From here until TTL expiry, every visitor sees $49 again, and the writer's logs insist the invalidation succeeded. It did. It was just too early.
Production fixes are boring on purpose: delete twice (once before the write, once shortly after, covering the read window), version keys per write so stale readers populate an abandoned generation, or route hot-key writes through a single serializer. Staleness windows get numbers too, with a 60s TTL and 1,000 writes per day, expected stale exposure is small; with the same TTL and 100 writes per second to one hot key, the cache is stale more often than fresh, and write-through stops being optional.
Reach for cache-aside with delete-on-write and a TTL backstop by default; write-through where stale reads are unacceptable; write-behind only for losable high-volume writes, and double-delete or versioned keys on the hot paths where read-write races actually bite. Freshness, write speed, and simplicity form a triangle, every strategy perfects two corners and bills on the third, so pick by which corner the workload can afford to lose. One last honesty note about write-through: it narrows the disagreement window from expiry-scale to race-scale but does not eliminate it, failures between the two writes still split the sides, concurrent writers can interleave cache-first versus database-first orderings, and a crashed writer mid-sequence leaves whichever side finished first holding the newer copy. That is why readers still carry short TTLs even under write-through.
Delete-on-write with a TTL backstop, double-deletes or versioned keys on hot paths, write-through where stale reads bill real money. Honest caches handle reads one key at a time. But one key can be read 50,000 times a second, and when its TTL lapses everywhere at once, 500 workers miss in the same 10ms and thunder onto the database with identical queries. The open question is how a hot key plus synchronized expiry turns a healthy cache into a full outage, and what desynchronizes the herd.