Caching Strategies
Date: 2026-08-17
Keeping a copy somewhere faster to reach. Every layer of a web stack has one, and the hard half is never storing the copy — it’s knowing when the copy is wrong, and having a plan for that before it happens.
A cache stores the result of an expensive operation so subsequent requests can skip it. Invalidation is removing or refreshing a copy that no longer matches the truth.
Where copies live
browser memory this page view
browser disk across visits
service worker programmable, offline
CDN edge per region
reverse proxy in front of the app
application memory per process
shared cache Redis — across
processes
database buffer the database's own
A single request may pass eight caches. When something stale appears on screen, the diagnostic question is which one — and the answer is usually found by working inward from the browser.
The patterns
CACHE-ASIDE (lazy)
read → miss → fetch → store → return
most common; first request is slow;
cache only holds what's asked for
READ-THROUGH
the cache fetches on miss itself
same behaviour, hidden in the layer
WRITE-THROUGH
write to cache AND store together
cache always fresh; writes slower
WRITE-BEHIND
write to cache, persist later
fast writes, risk of loss on crash
REFRESH-AHEAD
refresh popular entries before expiry
no stale, no miss penalty, more work
Cache-aside is the default and the one to assume unless told otherwise.
The invalidation approaches
TTL — expire after a fixed time
simple, predictable, ALWAYS somewhat
stale. Fine for most content
EXPLICIT PURGE — evict on change
accurate; needs a reliable hook from
whatever changes the data; a missed
purge is invisible
VERSIONED KEYS — never invalidate
/app.a3f9c.js
content changes → the KEY changes
→ old entry is simply never requested
← no staleness problem at all
Versioned keys are the only approach without an invalidation problem, which is why every build tool hashes filenames. Use it wherever you control the URL.
stale-while-revalidate
The pattern worth knowing by name, because it removes the usual trade:
Cache-Control: max-age=60,
stale-while-revalidate=600
0–60s fresh → serve from cache
60–660s stale → SERVE IT ANYWAY,
refresh in background
>660s serve nothing stale; fetch
Nobody ever waits for a refetch. The cost is that content can be up to a minute behind, which for a product listing is almost always the right trade — Content Delivery Networks.
What not to cache
personalised HTML basket, account,
"hello, name"
responses with
Set-Cookie distributes one session
to many users
anything behind
authorisation unless keyed per user
prices, if they
vary by customer
See: Dynamic Pricing
The catastrophic version is caching a personalised page at a shared layer — one user’s basket served to everyone. Cache-Control: private restricts a response to the browser cache only, and it is the flag that prevents this.
Cache stampede
When a popular entry expires and every concurrent request misses at once:
1,000 requests/second
key expires
→ 1,000 simultaneous origin requests
→ origin falls over
→ nothing repopulates the cache
→ it stays down
Fixes: a lock so only one request rebuilds, stale-while-revalidate so nobody waits, or jittered TTLs so keys don’t expire in lockstep. The last is the cheapest and catches most of it.
Measuring
HIT RATIO by content type. Low on
static assets is a config
bug, not a tuning problem
LATENCY hit vs miss — quantifies
the value
EVICTION RATE high means the cache is
too small
STALENESS how far behind, and does
anyone notice
The rule to carry
Decide the invalidation strategy before adding the cache. A cache added for speed with no answer to “how does this get updated” becomes a source of bugs that present as data problems and get investigated as data problems — and the layer responsible is the last place anyone looks.