Building Blocks · Caching

Caching Strategies

Where a cache sits and how it stays correct: read and write patterns, eviction, TTLs and invalidation, hot keys and stampedes, and CDNs.

  • 9 min read
  • 11 practice questions
On this page
  1. Description
  2. Where caches sit
  3. Getting data in: read patterns
  4. Writes
  5. Getting stale data out
  6. Eviction
  7. Stampedes and hot keys
  8. When the cache fails
  9. CDNs
  10. Which pattern?
  11. Check yourself
  12. Key takeaways
  13. Quick revise
  14. Practice

Description

A cache is a small, fast copy of data you'd otherwise fetch from somewhere slower. Picture a product page read 20,000 times a second while the product changes a few times a day: every read runs the same database query, against a database built for perhaps 3,000 queries a second. Keep a copy in memory and most of those reads never reach the database.

Adding Redis is easy and works on day one. Then the complaints start: a price change doesn't show up for an hour; a flash sale expires a hot key and the database falls over in the thirty seconds it takes to refill; the Redis node goes down and takes the database with it. Caching is easy to add and easy to get wrong, and the questions are always the same. In this lesson:

  1. Where caches sit - from the browser to the database.
  2. Getting data in (cache-aside and read-through) and handling writes.
  3. Getting stale data out - and why you delete rather than update.
  4. Eviction: what to throw away when memory fills up.
  5. Stampedes, hot keys, and what happens when the cache itself fails.
  6. CDNs, and which pattern fits which situation.

Where caches sit

Browser cache

CDN edge

App server memory

Shared cache
Redis / Memcached

Database

Browser cache

CDN edge

App server memory

Shared cache
Redis / Memcached

Database

Each layer answers what it can and passes the rest inward; the further in a request gets, the slower and more expensive it is.

  • In the client - browser caches, controlled by Cache-Control headers.
  • At the edge - a CDN caching static files, images and video close to users.
  • In the app server's memory - fastest, but each server has its own copy, so invalidation is per server.
  • A shared cache tier - Redis or Memcached, one copy shared by all app servers. This is what "add a cache" usually means.

Getting data in: read patterns

Cache-aside (lazy loading) is the default:

value = cache.get(key)
if value is None:
    value = db.query(key)
    cache.set(key, value, ttl=300)
return value

Only data someone actually asks for gets cached, and a cache failure degrades to database reads. Read-through is the same idea with the cache (or a library in front of it) doing the database load, so the app only talks to the cache.

Writes

PatternOn writeUpsideDownside
Write-around (with cache-aside)Write the database; delete or skip the cacheSimple; the cache holds only what's readThe next read is a miss
Write-throughWrite the cache and the database togetherCache always freshEvery write pays both; caches data nobody reads
Write-back (write-behind)Write the cache; flush to the database laterFastest writes; batches themWrites still only in the cache are lost if it fails

Write-back suits counters and metrics you can afford to lose a few seconds of - not orders or payments.

Getting stale data out

Delete, don't update. On a change, write the database and then delete the cache key; the next read loads the fresh value. Updating the key in place (cache.set(key, newValue)) races: two writers can commit to the database in one order and set the cache in the other, leaving the older value cached indefinitely:

CacheDatabaseWriter 2 (price 12)Writer 1 (price 10)CacheDatabaseWriter 2 (price 12)Writer 1 (price 10)database says 12, cache says 10 - until the TTL expiresUPDATE price = 10UPDATE price = 12SET price = 12SET price = 10
CacheDatabaseWriter 2 (price 12)Writer 1 (price 10)CacheDatabaseWriter 2 (price 12)Writer 1 (price 10)database says 12, cache says 10 - until the TTL expiresUPDATE price = 10UPDATE price = 12SET price = 12SET price = 10

Even delete-on-write has a rare race - a reader that loaded the old value just before the write can set it just after the delete. A TTL bounds how long any such mistake lives: pick one the data can tolerate (seconds for prices, hours for a blog post). And some reads shouldn't come from a long-lived cache at all: an account balance before a payment, stock at checkout, anything a decision depends on.

Eviction

Memory fills up. LRU (least recently used) evicts what hasn't been read for longest - good when recent popularity predicts future reads, as with trending content. LFU (least frequently used) favors keys with many hits over time. FIFO ignores popularity and evicts hot items just because they're old. A cache with no eviction simply fails writes (or runs out of memory) when full.

Stampedes and hot keys

Stampede (thundering herd). A popular key expires; in the next 50 ms, 5,000 requests miss and all query the database for the same row. Fixes:

  • Request coalescing - the first miss takes a short lock (or a single-flight promise) and reloads; the others wait for its result instead of querying.
  • Early refresh - reload a key shortly before it expires.
  • Jittered TTLs - 300 s ± 10%, so keys filled together don't expire together.

Hot keys. One key - a celebrity's profile - gets 500,000 reads a second, more than one cache node can serve. Consistent hashing spreads keys across nodes; it can't split one key. Replicate the hot key instead: store copies under profile:42#1..#8 and read a random one, or keep a short-lived (a few seconds) copy in each app server's memory.

When the cache fails

At a 90% hit rate the database sees only 10% of reads. If the cache disappears, the database suddenly gets all of them - ten times its normal read load - often exactly when you can least afford it. Plan for it: run the cache replicated across zones, keep the app able to reach the database without it (slower but correct), shed or rate-limit load while the cache warms back up, and don't size the database only for misses if an outage must stay survivable.

CDNs

A CDN is a cache on the edge of the network, close to users. Static assets, images and video segments are cached at hundreds of locations; Cache-Control headers and versioned file names (app.3f9a.js) control freshness. For media-heavy products it removes most bandwidth from your servers.

Which pattern?

SituationPattern
Read-heavy data that changes occasionally (products, profiles)Cache-aside, delete on write, TTL as a safety net
Data that must be fresh the moment it's written, read right afterWrite-through (or skip the cache for that read)
High-rate counters, metrics, view countsWrite-back, accepting a few seconds of loss
Decisions on money or stock (balance before a payment)Read the database, not the cache
Static files, images, videoCDN with versioned file names
One key with enormous trafficReplicated hot key, or a few seconds in app memory

Going further. The deep dive (next tab) quantifies hit ratios and what the last percent is worth, simulates LRU on skewed traffic, shows how Facebook closed the cache race at scale (leases and invalidation from the database's commit log), and covers eviction algorithms, Redis failover, cache penetration and avalanches, private data in shared caches, and HTTP's stale-while-revalidate.

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why delete the cache key on a write rather than setting the new value?

Show answer

Two concurrent writers can update the database in one order and the cache in the other, leaving the older value cached until it expires. Deleting makes the next reader load whatever the database holds now.

2. A cache has a 95% hit rate. It fails. What happens to database read load?

Show answer

It goes from 5% of reads to 100% - twenty times higher. A database sized for misses won't survive that without load shedding or a replicated cache.

3. When is write-back the wrong choice?

Show answer

When losing the most recent writes is unacceptable - orders, payments, anything durable - because writes that haven't been flushed are lost if the cache node fails.

Key takeaways

  • Cache-aside is the default: read the cache, load on a miss, fill the cache.
  • Write-through is fresh but slow to write; write-back is fast but can lose writes.
  • On writes: update the database, then delete the key; a TTL bounds staleness.
  • Evict with LRU (or LFU); FIFO evicts popular items.
  • Coalesce misses and jitter TTLs against stampedes; replicate hot keys.
  • A cache failure multiplies database load by 1 ÷ (miss rate) - plan for it.
  • CDNs cache static content and media at the edge.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before a design interview, and open the full lesson for anything that doesn't ring a bell.

Which pattern?

SituationPattern
Read-heavy data that changes occasionallyCache-aside, delete on write, TTL as a safety net
Must be fresh right after a writeWrite-through, or skip the cache for that read
High-rate counters, metrics, view countsWrite-back, accepting a few seconds of loss
Decisions on money or stockRead the database, not the cache
Static files, images, videoCDN with versioned file names (app.3f9a.js)
One key with enormous trafficReplicate the hot key, or a few seconds in app memory

Writes

PatternOn writeUpsideDownside
Write-around (with cache-aside)Write the DB; delete the keySimple; caches only what's readNext read misses
Write-throughWrite cache and DB togetherAlways freshEvery write pays both; caches unread data
Write-backWrite cache; flush laterFastest, batchedUnflushed writes lost on failure

Cache-aside in four lines

value = cache.get(key)
if value is None:
    value = db.query(key); cache.set(key, value, ttl=300)
return value

Failure patterns

PatternFix
Stampede - a hot key expires, thousands of misses at onceRequest coalescing (single-flight), leases, early refresh
Penetration - keys that don't exist always missCache "not found" briefly; Bloom filter; rate limits
Avalanche - many keys expire togetherJittered TTLs (300 s ± 10%); staggered warm-up
Hot key - one key beyond a node's capacityCopies under key#1..#8; local in-memory copy
Cold start / cache outageReplicate across zones; shed load; warm gradually

The traps

  • Updating the key on write - two writers can leave the older value cached. Delete instead.
  • No TTL - any race or missed invalidation lives forever.
  • FIFO eviction throws out popular items; no eviction fails writes when full.
  • Sizing the database only for misses - a cache outage multiplies its load by 1 ÷ miss rate (10× at 90% hits, 100× at 99%).
  • Write-back for orders or payments.
  • A cache key missing what the value depends on (user, language, currency) serves one user's data to another.

Staff-level points, one line each

  • Hit 0.5 ms, miss 5 ms + check: 90% → 1.00 ms avg, 98% → 0.60 ms, 99% → 0.55 ms. Latency barely moves, but each lost percent near the top doubles or triples database load.
  • LRU on Zipf traffic (100,000 items): a 1% cache hits 50.7% vs 61.9% ideal, a 10% cache 73.4% vs 81.0% - one-off scans (crawlers, reports) flush the hot set.
  • W-TinyLFU (Caffeine) admits a key only if a frequency sketch says it beats the victim; naive LFU never forgets; Redis samples (maxmemory-samples 5) to approximate LRU/LFU. Use allkeys-lru for a pure cache, never noeviction.
  • Delete-on-write still races (a slow reader sets the old value after the delete). Facebook's fix: leases (a delete invalidates the reader's token; also stops stampedes) and invalidation from the commit log (mcsqueal).
  • CDC (Debezium on the WAL) drives cache deletes, search updates and events from one source of truth - no forgotten invalidations.
  • Redis replicates asynchronously: a failover can lose the last writes - fine for a cache, dangerous for locks or write-back. Cluster = 16,384 slots; multi-key ops need a shared hash tag ({user:42}).
  • HTTP: private (no shared caches), no-store (nobody), Vary (key by a header); cache poisoning exploits unkeyed headers.
  • stale-while-revalidate serves stale while refreshing in the background; stale-if-error turns an origin outage into slightly old content.

Say it in an interview

  • "Cache-aside with delete-on-write and a short TTL; the checkout reads stock from the database."
  • "Coalesce misses and jitter TTLs, so a hot key expiring doesn't stampede the database."
  • "At 99% hits a cache outage is 100× database load - so a replicated cache and load shedding."
  • "Invalidation comes from the commit log, so a script writing directly can't leave stale data."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Caching Strategies free

Practice Caching Strategies free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime