Skip to main content
Cache Read Strategieslesson 4 of 4 · 2 min read

Staleness and TTLs

Two copies of the truth

The moment you cache a value you hold two copies of the truth, and they disagree the instant your database changes before your cache does.

Staleness is not a bug you can eliminate. It is a budget you set.

The expiry is your crude but dependable tool. An entry older than its expiry is thrown away, so that number is your worst case for serving old data.

Setting it is a product decision wearing an engineering costume. A username can be an hour stale and nobody files a ticket. A price on a checkout page five minutes stale becomes an escalation, and a stale stock count oversells.

Each cached field gets one question. What does somebody do with a wrong value, and what does that cost you?

Set the expiry to the largest number that answer tolerates, because longer expiries mean higher hit rates and less load on your database.

Closing the gap

Expiries leave a gap, since on average an entry sits stale for half its expiry after every write. If that is too long, delete the key inside the code path that writes to your database.

Delete, do not update. Updating from your application races with concurrent fills and can pin an old value in place, while a delete simply forces the next read to fetch fresh.

The expiry stays as a backstop, because invalidation code has bugs, deploys crash between the write and the delete, and messages get dropped. Your expiry guarantees any such mistake heals itself in bounded time.

A third trick sidesteps invalidation entirely: put a version in the key. Writes bump the version, readers fetch the new key, and the old entry dies by eviction. It costs memory for dead entries, and it works best when your row already carries a version.

the shape of it
Write landsDatabasenew priceCacheold priceReaders see oldthe windowExpiry ends it1. new value2. unchanged3. served old4. window closes
step 1 of 4
Staleness is a window you choose the length of, not a bug you remove.

Worked example

Priya's team caches seat availability for a cinema chain with a 60-second TTL, reasoning that shows rarely sell out in a minute. That holds until a Marvel premiere, when a 300-seat screen sells out in 40 seconds and the cache keeps advertising availability for most of another minute. Around 1,800 users tap into a checkout flow for seats that are gone, and 214 of them get an error after entering payment details. The redesign splits the data by volatility: static show metadata keeps a 10-minute TTL, the availability count drops to a 5-second TTL, and the final seat lock skips the cache and hits Postgres directly. Checkout errors from staleness fall to near zero, and the database barely notices the short TTL because only the hottest shows churn.

Cache Read Strategies: wrapping up

In the real world

  • 01Facebook's memcache paper (2013) describes cache-aside at enormous scale, with deletes on write and a leases mechanism to stop racing fills from setting stale values.
  • 02AWS DAX is a managed read-through and write-through cache for DynamoDB; applications swap the SDK client and misses are fetched from the table automatically.
  • 03Netflix runs EVCache, a memcached-based cache-aside layer replicated across availability zones, serving lookups like viewing history at millions of requests per second.
  • 04GitHub caches rendered HTML fragments in memcached with keys derived from record versions, so a push naturally generates new keys instead of requiring purges.
  • 05Cloudflare and other CDNs are TTL machines at planetary scale: the Cache-Control max-age header is the staleness budget idea applied to HTTP responses.

Questions people ask

Should I use cache-aside or read-through?

Default to cache-aside. It keeps the cache off the critical path, degrades to database reads if the cache dies, and lets you cache computed values with any key shape. Choose read-through when a managed product like DAX offers it on a store you already use, because then the consistency of a single fetch path comes nearly free.

What is a good cache hit rate?

It depends on the traffic pattern, but for read-heavy workloads with hot keys, 90 to 99 percent is common and achievable. More useful than the absolute number is the trend: alert when the hit rate drops several points below its baseline, because your database is sized assuming the miss traffic stays small.

Why delete the cache key on writes instead of updating it with the new value?

Updating races with concurrent cache fills. A reader that fetched the old database value can write it into the cache just after your update, pinning stale data until the TTL expires. A delete cannot lose that race the same way; the worst case is an extra miss, and the next read fetches fresh data.

Quick review

Cache-Aside (Lazy Loading):
app checks cache → miss → read DB → write to cache → return. App manages cache explicitly
The payoff in numbers:
Redis hit ~0.5 ms vs indexed DB read ~5 to 10 ms, a 10 to 20× win per hit. At 90% hit rate, DB read load drops 10×
Read-Through:
cache sits in front of DB. Cache fetches from DB on miss automatically. App only talks to cache
Cache-Aside miss storm:
on cold start or mass expiry, all requests hit DB simultaneously. Use probabilistic early expiry or locks
Cache-Aside pros:
only cache what's actually requested. Simple. DB failures don't cascade
Read-Through pros:
consistent caching logic, easier to reason about. Used by AWS ElastiCache + DAX (DynamoDB)
the trade-off

Cache-Aside: initial request always misses (cold cache). Read-Through: cache becomes SPOF if misconfigured.

in the room

Cache-Aside for most use cases. Maximum control. Read-Through when cache and DB are tightly coupled.