Course · 04

How does a cache make things fast, and how does it lie to you?

A cache keeps a copy of data close by, so you skip the slow trip. This page covers where caches live, how they fill and empty, how they go wrong, how HTTP and CDNs cache, and the math behind a hit ratio.

TL;DR

A cache trades freshness for speed: every design choice decides how stale a copy may get and who pays on a miss.

  • Cache-aside is the default: read the cache, on a miss read the DB and fill the cache; on a write, update the DB then delete the cache key.
  • Expired hot keys cause stampedes. Fix with one-fetch-per-key (locks, leases), early refresh, and random jitter on TTLs.
  • HTTP caching runs on Cache-Control (RFC 9111). CDNs like CloudFront serve from the nearest edge and go to your origin only on a miss.
  • Hit ratio decides everything: at 90% the DB sees 1 in 10 reads; at 99%, 1 in 100.
17K→ 1.3K/s
Facebook's peak DB query rate without and with memcache leases
1,000
Free CloudFront invalidation paths per month, per account
24h
CloudFront default TTL when you use no cache policy
5
Keys Redis samples per eviction by default (maxmemory-samples)

—Words you need

Each jargon word once, in plain words glossary

Cache
A fast, small copy of data kept near the reader.
Hit / miss
Hit: the copy was there. Miss: go to the real source.
Hit ratio
Hits divided by all lookups.
TTL
Time to live: how long a copy may be used.
Eviction
Throwing out entries because the cache is full.
Invalidation
Removing a copy because the real data changed.
Stale
A copy that is older than the real data.
Origin
The real source the CDN copies from.
Edge / POP
A CDN site near users that holds copies.
Cache key
The name used to find a copy, like a URL.
Stampede
Many requests miss at once and all hit the DB.
Bloom filter
Tiny structure that says "surely not here" or "maybe here".

Cache basics

01Why cache at all

Skip the slow trip, protect the slow thing core

Reading from memory is much faster than reading from disk or over a network. A database query also costs CPU, locks and connections. If many people read the same thing, it is wasteful to compute it again each time.

A cache keeps the answer. It does two jobs. It makes reads faster for users. It also protects the database from load it could not handle.

Speed
Answer from memory near the reader instead of from disk far away.
Protection
The DB sees only misses. At a 90% hit ratio it sees 10% of reads.
Cost
Memory is costly per GB, so a cache holds only the hot part.

Caching works when reads are far more common than writes, and when a little staleness is fine. It fails when every read is unique, or when data must be exact to the millisecond.

In everyday words
You keep the milk in your fridge, not at the shop. Most days you just open the fridge. You go to the shop only when the milk runs out or goes off.
Actually
Microsoft's Cache-Aside page says caching "works best for relatively static data or data that applications read frequently", and is not a good fit when "most requests don't experience a cache hit", because checking and filling the cache then costs more than it saves. Azure: Cache-Aside pattern

A cache is a bet that the same data will be read again soon. Check the bet with the hit ratio.

Source: Azure Architecture Center: Cache-Aside

02Where caches live

Six layers, from the user's browser down to the database core

Cache layers from user to database Six stacked layers: browser cache, CDN edge, reverse proxy cache, app in-memory cache, distributed cache such as Redis, and the database buffer pool. Browser cacheper user, Cache-Control CDN edgeshared, near users Reverse proxyNGINX proxy_cache App memoryone server only Distributed cacheRedis, Memcached Database buffer poolInnoDB, shared_buffers LEGEND User side Edge Servers Shared cache Database
Top is closest to the user and fastest. Bottom is closest to the data. A request stops at the first layer that has the answer.
LayerWho shares itWhat it holdsWho controls freshness
BrowserOne userPages, images, API repliesHTTP headers like max-age
CDN edgeAll users near that siteStatic files, some API repliesHTTP headers + CDN TTL settings
Reverse proxyAll users of that proxyWhole HTTP responsesProxy config + headers
App memoryOne server processObjects, config, hot lookupsYour code (TTL, size)
Distributed cacheAll app serversObjects, sessions, countersYour code (TTL, delete on write)
DB buffer poolThe databaseTable and index pagesThe database itself
In everyday words
Your pocket, your desk drawer, the office cupboard, the building store room. You check the nearest place first.
Actually
Even with no app cache, the database caches. MySQL InnoDB keeps table and index data in its buffer pool, and "on dedicated servers, up to 80% of physical memory is often assigned to the buffer pool". It uses an LRU variant with midpoint insertion: new pages go into an "old" sublist that is 3/8 of the pool, so one big table scan does not push out the hot pages. MySQL 8.4: InnoDB buffer pool

An in-process cache is fastest but each server has its own copy. A distributed cache is one shared copy, one network hop away.

Source: MySQL buffer pool · Azure Cache-Aside (local vs shared) · RFC 9111

03Cache-aside (lazy loading)

The default pattern: the app manages the cache itself interview favourite

Cache-aside read on a miss The app asks the cache for user 42 and gets a miss. It reads the row from the database, then sets the value in the cache with a TTL of 300 seconds. GET USER:42 MISS SELECT ROW ROW SET + TTL 300 Appyour code CacheRedis Databasesource of truth LEGEND App Cache Database Reply
On a hit, only the first arrow and a reply happen. The other trips happen only on a miss.

Read: try the cache. On a miss, read the DB, put the value in the cache with a TTL, and return it.

Write: update the DB first. Then delete the cache key. The next read fills it again.

cache-aside in pseudocode
def get_user(id):
    v = cache.get(f"user:{id}")
    if v is not None:
        return v                                   # hit
    v = db.query("SELECT * FROM users WHERE id=%s", id)  # miss
    cache.set(f"user:{id}", v, ttl=300)
    return v

def update_user(id, fields):
    db.update("users", id, fields)                 # 1. source of truth first
    cache.delete(f"user:{id}")                     # 2. then invalidate
GoodBad
Only data that is read gets cached.Every miss costs three trips: cache, DB, cache set.
If the cache dies, the app still works (slower).A fresh, empty cache sends a flood to the DB.
Simple, works with any cache store.Data can be stale until TTL or delete.
In everyday words
You check your notebook for a phone number. If it is not there, you look it up in the phone book and write it in the notebook for next time.
Actually
AWS calls this lazy loading: "Each cache miss results in three trips", and "Node failures aren't fatal for your application". Microsoft warns about order: "Update the data store before removing the item from the cache". If you delete first, a reader can miss, read the old row, and put stale data back. AWS ElastiCache: caching strategies

Cache-aside is the safe default: DB first, then delete the key, and always set a TTL as a safety net.

Source: AWS ElastiCache strategies · Azure Cache-Aside

04Read-through, write-through, write-behind, write-around

Who fills the cache, and when the database gets the write core

Read-through. Like cache-aside, but the cache library loads from the DB itself on a miss. The app only talks to the cache.

Write-through The app writes x equals 5 to the cache. The cache writes the same value to the database and waits. The database says OK, then the cache says OK to the app. WRITE X=5 WRITE X=5 OK OK, BOTH SAVED Appyour code Cachewrites through Databasesource of truth LEGEND App Cache Database Done only after both
Write-through: the caller waits for both writes. Cache and DB agree, but every write is slower.
Write-behind (write-back) The app writes x equals 5 to the cache and gets OK at once. Later the cache sends a batch of writes to the database. WRITE X=5 OK NOW BATCH, LATER Appyour code Cacheholds dirty data Databasegets it later LEGEND App Cache, at risk Database Async
Write-behind: fast writes, but if the cache dies before the batch, those writes are lost.

Write-around. Writes go straight to the DB and skip the cache. The cache fills only when someone reads. Good for data that is written once and rarely read, like logs.

PatternRead pathWrite pathFresh?RiskUse for
Cache-asideApp reads cache, then DB on missApp writes DB, deletes keyStale until delete or TTLMiss penalty, racesMost read-heavy apps
Read-throughCache loads from DB on miss(paired with one below)Same as cache-asideNeeds a cache that can loadLibraries, CDNs
Write-throughAlways in cacheCache and DB, caller waitsYes, after the writeSlower writes; caches data nobody readsRead-after-write needs
Write-behindAlways in cacheCache now, DB later in batchesCache yes, DB lagsData loss if cache diesCounters, likes, metrics
Write-aroundCache-aside styleDB onlyFirst read is a missRecent writes always missWrite-once, read-rarely data
In everyday words
Write-through is saving a document to your laptop and the cloud before you close it. Write-behind is saving to the laptop and letting it sync later, unless the laptop dies first.
Actually
AWS lists write-through's trade-offs: "Data in the cache is never stale", but "Every write involves two trips", and data written before a new node joined is missing ("Missing data"), plus "Cache churn" because "Most data is never read". AWS's advice: combine lazy loading and write-through, and add a TTL to every key. Write-behind is named in Microsoft's Cache-Aside page as an option some caches offer; it has no AWS-style guarantee, so the loss risk is yours to handle. AWS ElastiCache: caching strategies

The write pattern decides the freshness and the risk: write-through pays on writes, write-behind risks loss, cache-aside accepts brief staleness.

Source: AWS ElastiCache strategies · Azure Cache-Aside

05Eviction: LRU, LFU, FIFO, TTL

What to throw out when memory is full core

A cache is smaller than the data. When it is full and a new item comes, something must go. The eviction policy picks the victim.

PolicyThrows outGood whenWeak when
LRU (least recently used)The item not used for the longest timeRecent items are likely to be used again (most apps)One big scan pushes out all the hot items
LFU (least frequently used)The item used the fewest timesSome items stay popular for a long timeOld favourites hang on unless counts decay
FIFO (first in, first out)The oldest item addedVery simple; items have similar valueThrows out hot items just because they are old
TTL (time to live)Items whose time is upBounding stalenessNot a size limit on its own
RandomAny itemAll items accessed about equallyCan hit a hot item
LRU worked example · capacity 3 · newest on the right
get A   →  [A]
get B   →  [A, B]
get C   →  [A, B, C]
get A   →  [B, C, A]        # A used again, moves to newest
get D   →  [C, A, D]        evict B: least recently used
get C   →  [A, D, C]

Real LRU needs a hash map plus a doubly linked list. Both give O(1) get and put: the map finds the node, the list moves it to the front.

In everyday words
LRU is clearing out clothes you have not worn for longest. LFU is clearing out clothes you have worn the fewest times.
Actually
Redis does not run exact LRU. "It samples a small number of keys at random and then evicts the ones with the longest time since last access", with maxmemory-samples 5 by default. LFU uses a Morris counter (a tiny probabilistic counter) that saturates near one million hits and decays every minute (lfu-log-factor 10, lfu-decay-time 1). The default policy is noeviction: writes fail when memory is full. Redis docs call allkeys-lru "a good default option". Redis: key eviction

For a pure cache, set maxmemory and use allkeys-lru or allkeys-lfu. Never leave a cache on noeviction.

Source: Redis key eviction · MySQL buffer pool (scan-resistant LRU)

When caches go wrong

06Invalidation, stale data and consistency

The famous hard problem, step by step deep dive

A cache holds a copy. When the real data changes, the copy is now wrong. Invalidation is how you remove or fix it. There are three tools.

TTL
Let it expire
Simple safety net. Data can be wrong for up to the TTL.
Delete on write
Remove the key after a DB write
Fresh on next read. Needs every writer to remember.
Event-driven
Listen to DB changes
A change stream (CDC) deletes keys. Catches writers you forgot.

Why delete instead of update? Two writers can update the cache in the wrong order and leave the old value. A delete is safe to repeat; it is idempotent (doing it twice equals doing it once).

A real race in cache-aside

StepReader RWriter WCache holds
1Cache miss for xnothing
2Reads DB: x = 1nothing
3Writes DB: x = 2nothing
4Deletes key xnothing
5Sets cache x = 1 (late)x = 1, stale until TTL

Fixes: a short TTL limits the damage. A delayed second delete ("delete again after a short wait") catches late sets; this is a common practice, not a standard. Leases, below, close the gap properly.

In everyday words
A friend reads you an old menu, the restaurant changes it, then your friend writes the old prices on your fridge. Now the fridge is wrong until you throw the note away.
Actually
Facebook's memcache paper calls this a "stale set" and fixes it with leases. On a miss, memcached hands the client "a 64-bit token bound to the specific key". The set only works if the token is still valid; a delete for that key cancels the token, so the late stale set is refused. Facebook used "demand-filled look-aside" caching and chose deletes because "deletes are idempotent". Nishtala et al., Scaling Memcache at Facebook, NSDI 2013, §3.2.1

Delete, do not update; always keep a TTL; and if staleness really matters, use leases or versioned values.

Source: Scaling Memcache at Facebook (NSDI 2013) · Azure Cache-Aside (consistency)

07Stampede, penetration, avalanche, hot keys

Four ways a cache sends a flood to the database interview favourite

Cache stampede (thundering herd)

A hot key expires. A thousand requests miss at the same moment. All thousand run the same slow query. The DB falls over.

Request coalescing stops a stampede Three requests for the same expired key all reach a single-flight lock. Only one query goes to the database; the others wait for its result. 1 QUERY Request 1gets the lock Request 2waits Request 999waits Single flightone lock per key Databasesafe LEGEND Request for same key Coalescing point Database
Many misses, one query. Everyone else reuses its answer.
FixHowCost
Lock / leaseFirst miss takes a lock (for example Redis SET key NX PX 5000); others wait briefly or get the old value.Waiters add latency; lock needs a timeout.
Request coalescing (single flight)In one process or proxy, merge identical in-flight misses into one fetch.Per node only, unless done in a shared tier.
Probabilistic early expirationEach reader may refresh a little before expiry, with rising chance near the end.A few extra refreshes; no coordination.
Stale-while-revalidateServe the old value, refresh in the background.Users see slightly old data.
XFetch rule (Vattani et al., VLDB 2015) · delta = time to recompute, beta = 1 by default
if now - delta * beta * log(random()) >= expiry:   # log(random()) is negative
    recompute_and_reset()                           # chance rises as expiry nears

Cache penetration

Requests ask for keys that do not exist, like user:-1. Every one misses and hits the DB. Attackers can do this on purpose.

Fixes: negative caching (store "not found" with a short TTL) and a Bloom filter of all real IDs. A Bloom filter can say "surely not here" with no false negatives, so you skip the DB.

Cache avalanche

Many keys expire at the same moment, often because they were loaded together with the same TTL. Or the whole cache restarts empty. The DB gets everything at once.

Fixes: add jitter (random extra seconds) to TTLs, warm the cache before traffic, use replicas so one node loss does not empty it, and rate limit the DB path.

Hot keys

One key gets a huge share of traffic, like a celebrity's profile. It lives on one cache node, so that node melts while others are idle.

Fixes (common practice, not one standard): a small in-process cache in front for that key, copies of the key under suffixes (key#1 to key#8) read at random, and read replicas.

In everyday words
Stampede: the shop opens and everyone runs for the one item. Penetration: people keep asking for an item the shop never sells. Avalanche: every shelf empties at closing time. Hot key: one till has a famous cashier.
Actually
Facebook's leases also tame herds: memcached returns a token "only once every 10 seconds per key"; others wait and retry. In their test, "Without leases, all of the cache misses resulted in a peak database query rate of 17K/s. With leases, the peak database query rate was 1.3K/s." AWS's Builders' Library recommends a "negative cache" with "a different TTL than positive cache entries", and keeping "only one pending request" per uncached item. Redis Bloom docs: a Bloom filter "can guarantee the absence of an item", and a 1% error rate needs about 9.6 bits per item. Scaling Memcache at Facebook, §3.2.1

Every cache failure mode ends the same way, a flood on the DB. Coalesce misses, cache "not found", jitter TTLs, and spread hot keys.

Source: Memcache at Facebook · AWS Builders' Library: Caching challenges · Vattani et al., VLDB 2015 · Redis Bloom filter

The web edge

08HTTP caching headers

How a server tells browsers and CDNs what they may keep core

HTTP has caching built in. The server adds headers to the response. Every cache on the path, browser or CDN, reads them and obeys.

Header / directiveMeaningExample
Cache-Control: max-age=NFresh for N seconds after it was mademax-age=604800 (7 days)
s-maxage=NLike max-age, but only for shared caches (CDN, proxy); overrides max-age theremax-age=60, s-maxage=3600
no-cacheMay store, but must check with the server before each reuseHTML pages that change often
no-storeDo not store at allBank statements
private / publicOnly the browser may store / shared caches may store tooprivate for a user's profile
immutableWill never change while fresh; skip revalidationVersioned files like app.3f9a.js
stale-while-revalidate=NAfter expiry, may serve stale for N s while refreshing in the backgroundmax-age=600, stale-while-revalidate=30
stale-if-error=NIf the origin errors, may serve stale for N sstale-if-error=86400
ETag + If-None-MatchA version tag; if it still matches, server replies 304 Not Modified with no bodyETag: "33a64df5"
Last-Modified + If-Modified-SinceSame idea using a dateStatic files
revalidation with an ETag
# first visit
GET /app.css                       →  200 OK
                                       Cache-Control: max-age=600
                                       ETag: "v42"
# 15 minutes later: stale, so the browser asks "is v42 still right?"
GET /app.css
If-None-Match: "v42"               →  304 Not Modified   (no body, reuse the copy)
In everyday words
max-age is a "best before" date. An ETag is a version number: you ask the shop "is version 42 still current?" and they just nod.
Actually
RFC 9111 §4.2: a response is fresh when freshness_lifetime > current_age. The cache picks the lifetime from the first that applies: s-maxage (shared caches only), then max-age, then Expires minus Date, then a heuristic; a typical heuristic is 10% of the time since Last-Modified. stale-while-revalidate and stale-if-error come from RFC 5861. MDN: no-cache "does NOT mean 'don't cache'"; it means revalidate first. RFC 9111 §4.2 Freshness

Version your static files and give them a long max-age with immutable; give HTML no-cache plus an ETag.

Source: RFC 9111 HTTP Caching · RFC 5861 · MDN Cache-Control · MDN ETag

09CDNs and how CloudFront works

Copies of your files in cities near your users deep dive

How CloudFront serves a request A viewer sends GET to the nearest edge POP. On a miss the POP asks a regional edge cache. On another miss the regional cache fetches from the origin. GET MISS MISS Viewerbrowser Edge POPnearest by latency Regionalbigger cache OriginS3 or server LEGEND Viewer Edge, most hits Mid tier Your origin
Each tier caches the object on the way back, so the next viewer nearby gets a hit.

A CDN (content delivery network) is many cache servers around the world. The edge is the site near the user. The origin is your real server or bucket.

IdeaWhat it means
Pull CDNThe CDN fetches from the origin on the first miss, then caches. You upload nothing to the CDN. CloudFront works this way.
Push CDNYou upload files to the CDN's storage ahead of time. Good for big files you know will be wanted. (General industry term; not a CloudFront mode.)
Cache keyWhat makes two requests "the same object". CloudFront's default: distribution domain + URL path. Adding headers, cookies or query strings splits the cache.
InvalidationTell the CDN to drop objects before their TTL ends. Costs money past a free tier and takes time to spread.
Versioned URLsPut a hash in the file name (app.3f9a.js). A new deploy is a new URL, so no invalidation is needed.
Origin shieldOne extra cache tier in front of the origin, so all edges share one copy and the origin sees fewer requests.
In everyday words
A pull CDN is a corner shop that orders an item from the warehouse the first time a customer asks. A push CDN is a warehouse that stocks every shop before opening day.
Actually
CloudFront: "DNS routes the request to the CloudFront POP (edge location) that can best serve the request, typically the nearest CloudFront POP in terms of latency." On a miss the POP "typically goes to the nearest regional edge cache", which has "a larger cache than an individual POP". Dynamic requests and PUT/POST skip regional caches. "The first 1,000 invalidation paths that you submit per month are free", and a /* wildcard counts as one path. Without a cache policy the default TTL is 24 hours. Origin Shield consolidates misses, "resulting in as few as one request going to your origin". As of these docs, checked 2026-10-25. AWS: How CloudFront delivers content

Keep the cache key small, version your file names, and treat invalidation as an emergency tool, not a deploy step.

Source: CloudFront: how it works · CloudFront: cache key · CloudFront: invalidation pricing · CloudFront: expiration · CloudFront: Origin Shield

10Cache hit ratio math

One worked example you can redo in an interview worked example

90of 100
Example: lit squares are reads served by the cache. Only the 10 dark squares reach the database.

Hit ratio = hits ÷ (hits + misses). The numbers below are an example for practice, not a measurement.

Input (example)Value
Reads per second10,000
Cache read time1 ms
Database read time10 ms
Hit ratio90%
DB reads/s    = 10,000 × (1 − 0.90)            = 1,000
avg latency   = 0.90 × 1 ms + 0.10 × (1 + 10) ms
              = 0.9 + 1.1                     = 2.0 ms     # a miss pays the cache check too

at 99% hits:  DB reads/s = 100,   avg = 0.99 × 1 + 0.01 × 11 = 1.1 ms

Database reads per second at each hit ratio

Example of 10,000 reads per second. Hover a bar for the exact number.

0% hits
10,000
50% hits
5,000
90% hits
1,000
99% hits
100
Show the full table
Hit ratioDB reads/sAverage latency (ms)
0%10,00011.0
50%5,0006.0
90%1,0002.0
99%1001.1

Look at the jump from 90% to 99%. The hit ratio rose 9 points, but DB load fell 10 times. Near the top, every point matters.

In everyday words
If 9 of 10 questions are answered by the receptionist, the manager gets 1 in 10. If the receptionist answers 99 of 100, the manager's day is 10 times quieter again.
Actually
Redis reports the inputs: keyspace_hits and keyspace_misses in INFO stats, and the docs give the formula keyspace_hits / (keyspace_hits + keyspace_misses) * 100. If the ratio is low, check evicted_keys (cache too small or wrong policy) and expired_keys (TTL too short). Redis: key eviction, using INFO

Size the DB for the miss traffic, and remember the miss traffic jumps the moment the cache is cold.

Source: Redis eviction docs (INFO stats) · math is a worked example on this page

Practice

—When to use what

Situation, pick, why decision table

SituationPickWhy
Read-heavy data, a few seconds of staleness is fineCache-aside + TTL, delete on writeSimple, survives cache loss.
Users must see their own write at onceWrite-through (or delete + read from primary)Cache updated in the same write.
Very high write rate of counters, likes, viewsWrite-behind with batchingAbsorbs writes; accept small loss risk.
Logs or data written once, rarely readWrite-aroundKeeps the cache for hot data.
General purpose cache in Redisallkeys-lruDocs call it a good default.
Some items stay popular for daysallkeys-lfuKeeps long-term favourites.
Hot key expiry causes DB spikesLock/lease or early refreshOne fetch per key.
Bots ask for IDs that do not existNegative cache + Bloom filterMisses for "not found" stop at the cache.
Static JS, CSS, imagesCDN, hashed file names, max-age=31536000, immutableNo invalidation needed; max edge hits.
HTML that changes oftenno-cache + ETag, or a short s-maxage with stale-while-revalidateFresh, but cheap 304s.
Per-user dataCache-Control: privateNever stored in shared caches.

—Common mistakes and traps

Things that look right and break in production traps

Delete before write
Deleting the cache key, then updating the DB
A reader refills the old value in the gap. Update DB first.
No TTL
Keys that live forever
One missed invalidation means wrong data forever.
Same TTL for a batch
10,000 keys loaded together, all expire together
Avalanche. Add random jitter.
noeviction cache
Redis default policy left on
When full, writes fail instead of old keys leaving.
Caching personal data publicly
No private on a user page
A CDN can serve one user's page to another. A real data leak risk.
Fat cache key
Adding User-Agent or session cookies to the CDN key
Thousands of copies of one file; hit ratio collapses.
no-cache misread
Using no-cache to mean "never store"
It means "revalidate". Use no-store for secrets.
Cold start
Cache restarts empty during peak
DB gets the full load. Warm up, or restart nodes one by one.

—Interview questions

20 questions, easy to design-level. Try first, then open the answer drill

1. Why do we cache, and when should we not? easy
Show answer
To serve repeated reads from fast memory near the reader, and to shield slow systems like the database. At a 90% hit ratio the DB sees only 10% of reads. Do not cache when reads are mostly unique (low hit ratio), when data must be exact right now (balances at the moment of payment), or when the data is sensitive and the cache is shared. Microsoft's guidance says caching "works best for relatively static data or data that applications read frequently". Follow-up: "How do you know it's working?" Watch hit ratio, evictions and DB load.
2. Walk through cache-aside for reads and writes. easy
Show answer
Read: check the cache; on a hit return it; on a miss read the DB, store it with a TTL, and return it. Write: update the DB first, then delete the key, so the next read refills with fresh data. Pros: only read data is cached, and the app still works if the cache dies. Cons: each miss costs three trips (AWS's words) and data can be stale until delete or TTL. Order matters: Microsoft notes that deleting first lets a reader put the old value back. Always set a TTL as a safety net.
3. Why delete the cache key on write instead of updating it? medium
Show answer
Two writers can race: W1 writes DB = 1, W2 writes DB = 2, then W2 sets cache = 2 and W1 sets cache = 1 late. Now the cache holds 1 forever (until TTL). A delete has no value to get out of order, and it is idempotent: deleting twice is the same as once. Updating also wastes work if the value is never read again, and computing the cached form may be costly. Facebook's memcache paper chose deletes for these reasons. Remaining gap: a reader can still set a stale value after the delete; leases fix that.
4. Compare write-through, write-behind and write-around. medium
Show answer
Write-through writes the cache and DB together and the caller waits for both: fresh reads, slower writes, and the cache fills with data nobody reads. Write-behind writes the cache and returns at once; the DB is updated later in batches: very fast writes, but data is lost if the cache dies first, and the DB lags. Write-around writes only the DB; the cache fills on read: keeps the cache for hot data, but a read right after a write misses. Use write-through for read-after-write needs, write-behind for counters, write-around for logs.
5. LRU vs LFU: which and why? medium
Show answer
LRU evicts the item unused for the longest time. It suits most apps, where recent items get used again, but a one-time scan can push out hot items. LFU evicts the item used least often; it keeps long-term favourites but needs decay, or yesterday's hit stays forever. Redis offers both, approximated by sampling: maxmemory-samples is 5 by default, and LFU uses a Morris counter that decays every minute. Redis docs call allkeys-lru a good default. I'd start with LRU and switch to LFU if hit ratio data shows stable popular items being evicted.
6. Implement an LRU cache. What data structures? medium
Show answer
A hash map from key to a node in a doubly linked list. The list is ordered by recent use: head is newest, tail is oldest. Get: look up the node in the map, move it to the head, return it, all O(1). Put: if the key exists, update and move to head; else create a node at the head and add it to the map; if over capacity, remove the tail node and its map entry. O(1) because the map finds nodes and the list removes or moves them without searching. In Java, LinkedHashMap with access order does this.
7. What is a cache stampede and how do you prevent it? medium
Show answer
A hot key expires and many requests miss together, all running the same expensive query, which can knock over the DB. Fixes: a per-key lock or lease so only one request recomputes while others wait or get the old value; request coalescing (single flight) in a process or proxy; probabilistic early expiration (XFetch) so one reader refreshes slightly before expiry; and stale-while-revalidate. Facebook's leases returned a token once every 10 seconds per key; peak DB queries fell from 17K/s to 1.3K/s in their test. Follow-up: lock timeouts so a dead holder does not block everyone.
8. What is cache penetration? How do you stop it? medium
Show answer
Requests for keys that do not exist in the DB at all, like random IDs. The cache never stores anything, so every request reaches the DB. Attackers can use this. Fixes: negative caching, which stores "not found" with a short TTL (AWS recommends a different TTL than for real entries); a Bloom filter of valid IDs, which answers "definitely not present" with no false negatives, so you skip the DB; and input validation plus rate limits. A Bloom filter at 1% false positives needs about 9.6 bits per item, so 100 million IDs take about 120 MB.
9. What is a cache avalanche? easy
Show answer
Many keys expire at the same time, or the whole cache goes down or restarts empty, so a huge wave of misses hits the DB. It often happens when a batch job loads many keys with the same TTL. Fixes: add random jitter to TTLs (for example 300 s plus up to 60 s random), warm the cache before switching traffic, run the cache with replicas and failover, restart nodes one at a time, and protect the DB with rate limits or load shedding. Serving stale data while refreshing also softens the wave.
10. How do you handle a hot key? hard
Show answer
A hot key lives on one cache shard, so that shard maxes out while others idle. First detect it with per-key access stats at the client or proxy. Then: put a small in-process cache with a short TTL in front, so most reads never leave the app server; replicate the key as key#1 to key#N across shards and read one at random (writes must update or delete all copies); use read replicas. For writes to a hot counter, shard the counter and sum on read. These are common practices rather than one standard.
11. How long should a TTL be? easy
Show answer
As long as the business can tolerate stale data, and no longer. A product description might take an hour; a stock price, seconds; a feature flag, a minute. Too short and the hit ratio falls and the DB works harder; Microsoft warns that premature expiry makes apps "continually retrieve data". Too long and users see old data after a missed invalidation. Combine a TTL with delete-on-write, so the TTL is only the safety net. Add jitter so keys loaded together do not expire together. Measure: if expired_keys is high and hit ratio low, the TTL is too short.
12. Explain Cache-Control max-age, s-maxage, no-cache and no-store. easy
Show answer
max-age=N: fresh for N seconds in any cache. s-maxage=N: same but only for shared caches like CDNs, and it overrides max-age there, so you can keep a page 1 hour on the CDN but 1 minute in browsers. no-cache: may store, but must revalidate with the server before each use, usually via ETag and a cheap 304. no-store: never store, for secrets. RFC 9111 picks freshness in order s-maxage, max-age, Expires, then a heuristic. Common mistake: using no-cache when you meant no-store.
13. How do ETags work? easy
Show answer
The server sends an ETag, a version tag for the response, like "v42" or a content hash. When the cached copy goes stale, the client asks again with If-None-Match: "v42". If the version still matches, the server replies 304 Not Modified with no body, and the client reuses its copy. That saves bandwidth, not the round trip. Strong ETags mean byte-identical; weak ones (W/"…") mean "same meaning". ETags are defined in RFC 9110. They also help with safe updates via If-Match.
14. How does a CDN like CloudFront serve a request? medium
Show answer
DNS sends the viewer to the CloudFront POP that can serve it best, usually the lowest latency one. The POP checks its cache by the cache key (by default the distribution domain plus URL path). On a hit it returns at once. On a miss it usually asks a regional edge cache, which is bigger. On another miss, it fetches from the origin, streams the first bytes to the user right away, and caches the object at both tiers. Dynamic requests and PUT/POST go straight to the origin. Origin Shield can add one more tier so the origin sees as few as one request per object.
15. Push CDN vs pull CDN? easy
Show answer
Pull: the CDN fetches from your origin on the first miss and caches it; you configure once and upload nothing. Simple, best for most sites. The cost: the first user in each region pays the miss, and unpopular files may be evicted. Push: you upload content to the CDN ahead of time. Good for big, predictable files like a game patch or video library, and when the origin cannot take miss traffic. You manage uploads and deletions yourself. CloudFront is a pull CDN by design: it goes to the origin on a miss.
16. How do you deploy new static files without serving old ones from the CDN? medium
Show answer
Use versioned or hashed file names, like app.3f9a.js, set by the build. Serve them with Cache-Control: max-age=31536000, immutable. The HTML that references them gets no-cache or a short TTL, so browsers pick up the new names quickly. A new deploy means new URLs, so nothing needs invalidating, and old pages still find their old files. Invalidation is slower and costs money: CloudFront gives 1,000 free paths per month, then charges per path. Keep invalidation for emergencies like a leaked file.
17. Your cache hit ratio dropped from 95% to 70%. What do you check? hard
Show answer
First the impact: DB reads went from 5% to 30% of traffic, a 6× jump. Then causes. Evictions: did evicted_keys rise? The working set may have grown past memory, or a scan job polluted LRU. Expiry: did someone shorten a TTL (expired_keys)? Key changes: a deploy that changed the key format or added a version makes every key new. Traffic mix: a new feature reading unique keys, or a bot hitting random IDs (penetration). Infrastructure: a node restart or resharding emptied part of the cache. For a CDN, check whether a header or cookie was added to the cache key.
18. Design the caching for a product page on a big shopping site. hard
Show answer
Split by how fast data changes. Images and JS: CDN, hashed names, one-year immutable. Product description: cache-aside in Redis with a TTL of minutes and delete on update through a change event. Price and stock: short TTL (seconds) or read from the DB for checkout, because wrong prices cost money; show "only a few left" from cache but check stock at order time. HTML shell: CDN with short s-maxage and stale-while-revalidate, personal parts loaded separately with private. Protect hot items in a sale with locks and an in-process cache. Measure hit ratio per layer.
19. What breaks if the Redis cache cluster goes down? hard
Show answer
With cache-aside, reads fall through to the DB. If the DB was sized only for misses, at a 95% hit ratio it suddenly sees 20× its normal read load and may fall over: the cache failure becomes a DB outage. Also lost: anything stored only in the cache, like sessions or write-behind data. Mitigations: replicas with automatic failover, timeouts so a dead cache fails fast, a circuit breaker and rate limit on the DB path, serving stale or degraded pages, and warming the cache gradually on recovery. AWS calls this the cache's "modal behavior" risk.
20. How do you keep a cache consistent with the database? hard
Show answer
Perfect consistency is not possible without making the cache part of the write path; choose the staleness you can accept. Practical stack: update DB then delete the key; a TTL on every key as a backstop; change-data-capture from the DB log to delete keys any writer forgot; leases or version numbers so a late stale set is rejected (Facebook's memcache leases cancel the token on delete). For read-after-write, read from the primary for that user briefly, or use write-through. Say the trade-off clearly: more consistency means more write cost or less hit ratio.

—Cheat sheet

Numbers and facts to remember memorise

FactValueSource
Cache-aside miss cost3 trips: cache, DB, cache setAWS ElastiCache strategies
Cache-aside write orderUpdate DB, then delete keyAzure Cache-Aside
Redis default eviction policynoevictionRedis eviction docs
Redis LRU samplingmaxmemory-samples 5Redis eviction docs
Redis LFU defaultslfu-log-factor 10, lfu-decay-time 1 (minute)Redis eviction docs
Hit ratio from Redishits ÷ (hits + misses), INFO statsRedis eviction docs
InnoDB buffer poolUp to 80% of RAM on dedicated servers; old sublist 3/8MySQL 8.4 docs
Facebook leasesToken once per 10 s per key; peak DB 17K/s → 1.3K/sNSDI 2013 paper §3.2.1
Bloom filter, 1% false positivesAbout 9.6 bits per item, 7 hash functionsRedis Bloom docs
Freshness orders-maxage → max-age → Expires → heuristicRFC 9111 §4.2
Typical heuristic freshness10% of time since Last-ModifiedRFC 9111
stale-while-revalidate, stale-if-errorDefined in RFC 5861RFC 5861
CloudFront default cache keyDistribution domain + URL pathCloudFront cache key docs
CloudFront default TTL (no cache policy)24 hoursCloudFront expiration docs
CloudFront free invalidations1,000 paths/month; /* = 1 pathCloudFront invalidation pricing
Hit ratio ruleDB load = reads × (1 − hit ratio)Worked example

—Sources

All checked 2026-10-25 references

WhatWhere
Cache-aside steps, write order, consistencylearn.microsoft.com: Cache-Aside pattern
Lazy loading, write-through, TTL trade-offsdocs.aws.amazon.com: ElastiCache caching strategies
Negative caching, coalescing, soft/hard TTL, modal behaviorAWS Builders' Library: Caching challenges and strategies
Redis eviction policies, LRU sampling, LFU, INFO statsredis.io: Key eviction
Bloom filter guarantees and sizingredis.io: Bloom filter
InnoDB buffer pooldev.mysql.com: InnoDB buffer pool
Leases, stale sets, thundering herds, 17K/s → 1.3K/sNishtala et al., Scaling Memcache at Facebook, NSDI 2013
Probabilistic early expiration (XFetch)Vattani, Chierichetti, Lowenstein, VLDB 2015
HTTP caching, freshness, heuristicRFC 9111
stale-while-revalidate, stale-if-errorRFC 5861
Cache-Control directivesMDN: Cache-Control
ETag and 304MDN: ETag
CloudFront request flow and regional edge cachesdocs.aws.amazon.com: How CloudFront delivers content
CloudFront cache keydocs.aws.amazon.com: Understand the cache key
CloudFront TTLs and stale directivesdocs.aws.amazon.com: Expiration
CloudFront invalidation pricingdocs.aws.amazon.com: Pay for file invalidation
Origin Shielddocs.aws.amazon.com: Origin Shield