Five cache strategies and the bugs each one has
Cache-aside is the default; write-through, write-behind, read-through and refresh-ahead exist for specific shapes.
What you will be able to do
- Distinguish cache-aside from read-through and write-through
- Explain the race that makes cache-aside write-then-delete fragile
- Choose a strategy for read-heavy, write-heavy, and write-then-read systems
- Describe a stampede and how to prevent it
Cache-aside (lazy loading)
Cache-aside is the default because failures are contained: if Redis is down, reads go to the database and the site is slow but correct. It also caches only what is actually requested, so memory goes to the working set.
The other four
| Strategy | Write path | Read path | Cost |
|---|---|---|---|
| Cache-aside | DB write, then delete key | Cache, else DB then fill | First reader after a write pays a miss |
| Read-through | Same as aside | Library loads on miss automatically | Hides the miss; still lazy |
| Write-through | Write cache and DB together | Always a hit after write | Every write pays cache latency; cache holds data nobody reads |
| Write-behind | Write cache, flush to DB later | Fast, always warm | Data loss on crash; ordering and retry complexity |
| Refresh-ahead | TTL plus background refresh before expiry | Rarely misses | Extra work on keys that may not be read again |
Write-behind is the one that needs a warning. It turns the cache into a write buffer, so a crash can lose everything not yet flushed. It is defensible for counters and metrics where a few lost increments are acceptable; it is not defensible for orders.
Stampedes and hot keys
Three failure shapes recur, and naming them is what separates a memorised answer from an engineering one.
- Stampede — a popular key expires, thousands of requests miss simultaneously and hammer the database. Fix: a per-key mutex so one loader runs, the rest wait; or serve slightly stale data while refreshing.
- Hot key — one key (a celebrity, a flash sale) absorbs most traffic and exceeds a single Redis node's capacity. Fix: replicate the key across nodes with a random suffix, or cache it in-process at the edge of each service.
- Cache penetration — requests for keys that do not exist always miss and hit the database. Fix: cache the negative result briefly, or use a bloom filter to reject impossible keys.
get(key):
value = cache.get(key)
if value: return value
if not mutex.acquire(key, ttl=2s):
return stale_copy(key) or db_read_with_timeout(key)
try:
value = db.read(key)
cache.set(key, value, ttl=300 + rand(0,60)) # jitter
then
return value
finally: mutex.release(key)TTL jitter matters more than it looks: if a deploy warms a thousand keys with the same TTL, they all expire in the same second an hour later. Randomising the TTL spreads the reload.
Worth remembering
- Cache-aside: application reads cache, loads on miss, and writes the database then invalidates.
- Write-through keeps the cache current at the cost of every write paying cache latency.
- Write-behind is fast and risky: a crash loses writes that were only in memory.
- Delete on write rather than update; concurrent updates leave stale values behind.