Invalidation strategies
TTLs, write-through, and why invalidation is one of the two hard problems.
5 minute lesson
Every cache is a copy. Copies go stale.
When the database changes, the cached value doesn't know. Until something removes or refreshes it, readers get the old answer. How long that can last is your staleness window.
- App reads$19from cache
- Cache$19stale
- Database$24updated
- TTL
- Time to live: how long a value may be kept, in a cache or a store, before it expires.
- invalidation
- Removing or refreshing a cached value when its source changes.
- staleness window
- How long readers can see an old value after the source changes.
Key terms
There are only two hard things in computer science: cache invalidation and naming things.
Strategy 1: let it expire
Each entry carries a time-to-live (TTL). When it expires, the next read misses and reloads from the database.
- t = 0 s
A read misses, loads $19 from the database and caches it for 60 s.
- Cache$19fresh
- Database$19source of truth
- 0 s · cached
- 20 s · DB change
- 60 s · expires
fresh
- t = 20 s
The price changes to $24 in the database. The cache still says $19.
- Cache$19stale
- Database$24source of truth
- 0 s · cached
- 20 s · DB change
- 60 s · expires
fresh, then stale
- t = 60 s
The entry expires. The next read caches $24. Readers saw a stale price for 40 s.
- Cache$24fresh
- Database$24source of truth
- 0 s · cached
- 20 s · DB change
- 60 s · expires
fresh, then stale
Prices are cached with a 5-minute TTL. A price changes. What's the worst case for shoppers?
- They see the old price for up to 5 minutes
- They get an error until the entry expires
- They see the new price immediately
- The price update fails while the entry is cached
Show the answer
- They see the old price for up to 5 minutes
TTL caps how long a stale value can live (here, 5 minutes). It doesn't cause errors or block writes.
A TTL bounds staleness. It doesn't remove it.
Strategy 2: delete on write
async function updatePrice(id, price) {
await db.product.update({
where: { id }, data: { price },
});
await cache.del(`product:${id}`);
}- Lines 2 to 4: Write the source of truth. Update the database first. If this fails, the cache still matches what is stored.
- Line 5: Then drop the cached copy. Delete the key instead of writing the new value. The next read misses and loads the fresh price.
- Line 5: Why not delete first?. A read between the delete and the write would re-cache the old price. Keep a TTL as a safety net for races and missed deletes.
Pick the right strategy
TTL only
expire
- Staleness
- Up to the TTL
- Write cost
- None
- Use when
- A bit stale is fine: feeds, counts
Delete on write
invalidate
- Staleness
- Near zero
- Write cost
- 1 extra call
- Use when
- You own every write path
Write-through
update
- Staleness
- Near zero
- Write cost
- Every write hits the cache
- Use when
- Data is read right after it's written
After writing a new price to the database, the service deletes the cache key instead of writing the new price into the cache.
Why delete rather than update?
- Deleting a key is faster than writing one
- Two writers updating the cache can finish in the wrong order and leave the older price there; a delete just makes the next read reload
- Caches can't store values that change
- Updating the cache would turn off the key's TTL
Show the answer
- Two writers updating the cache can finish in the wrong order and leave the older price there; a delete just makes the next read reload
If two requests write the price, each then updating the cache, the cache writes can arrive in the opposite order to the database writes. The cache then holds the older price until the TTL. Deleting has no such race: the next read loads whatever the database holds.
Delete on write, don't update: a delete can't leave an older value behind.
What should you pair with delete-on-write, and why?
Show the answer
A TTL. If a delete is ever missed (a crash, a race, a forgotten write path), the stale entry still expires on its own.
Recap
- TTL bounds staleness. It never removes it.
- Delete the cache key after the database write, not before.
- Pair delete-on-write with a TTL as a safety net.