CMD Guide
HomeSystem DesignCaching

Cache Invalidation

Cache invalidation is the set of rules that decide when a cached copy no longer matches its source of truth, so that the next read either removes it, refreshes it, or serves it while re-fetching — the cache is never allowed to silently keep returning a value the origin has since changed.

Two mechanisms are constantly confused, so pin them down before anything else:

This page is about the second. Write policies are a separate topic; listing them as "invalidation schemes" is the classic category error. Invalidation methods themselves split cleanly into two families:

The methods, by mechanism

Purge (event-driven, eager)

A hard delete of a specific key/URL. The entry is dropped from the cache immediately; the very next request for it is a guaranteed miss that goes to the origin, repopulates, and returns fresh. You must name each key you want gone.

Refresh (event-driven, eager)

Force a re-fetch from origin and overwrite the cached value in place, without deleting it first. Unlike purge, there is no window where the key is absent — a concurrent reader that arrives mid-refresh still gets a (stale) hit instead of a miss, so refresh avoids the miss-storm that mass purges cause.

Ban (event-driven, lazy)

Invalidate by criteria — a URL pattern or header — instead of by exact key. Issuing a ban does not touch any object; it appends a rule to a ban list stamped with the time it was issued. From then on, every incoming read is checked against the list: if the requested object matches a rule and was cached before that rule's timestamp, it is treated as stale and re-fetched; otherwise it is served normally. Objects are cleaned lazily as they happen to be requested, and a rule can be dropped once every object older than it has been refreshed. One O(1) command can invalidate millions of keys.

TTL expiration (expiry-driven)

Each entry carries a time-to-live. A read served before expiry is a hit; after expiry the entry is stale and the next read re-fetches. No coordination with writers at all — the cache heals itself on a fixed clock, at the cost of serving stale data for up to one TTL.

Stale-while-revalidate (expiry-driven)

On a read of an expired entry, serve the stale copy immediately and kick off an asynchronous re-fetch; the fresh value replaces the stale one when it lands. The reader never blocks on the origin — freshness is traded for latency.

diagram
diagram

Worked example: ending a holiday sale with one Ban

A CDN caches 40,000 product pages under /category/holiday-sales/…. At 09:00 the sale ends and every price under that path changes. Firing 40,000 individual purges would hammer the origin and the cache's control plane, so instead you issue one ban: pattern ^/category/holiday-sales, timestamped 09:00:00. Here is what the next few reads actually do:

TimeRequestObject's cached_timeBan checkResult
09:00:00ban issuedrule appended to ban listno objects touched
09:00:03GET /category/holiday-sales/red-scarf08:45:00matches & 08:45 < 09:00 → stalefetch origin, re-cache at 09:00:03, serve fresh
09:00:05GET /category/new-arrivals/blue-hat08:50:00no pattern matchserved from cache (fast HIT)
09:00:08GET /category/holiday-sales/red-scarf09:00:03matches but 09:00:03 > 09:00:00 → freshserved from cache (fast HIT)
~09:40last matching object finally re-fetched> 09:00:00no object can still trip the ruleban rule garbage-collected

The cost of invalidation is now paid lazily, per read, only for objects that are actually requested — and only once each. Cold objects nobody asks for are never re-fetched at all; their rule simply sits in the list until GC.

Pitfalls

Invalidate-then-write races (the classic correctness bug)

The intuitive order — delete the cache, then update the DB — is wrong under concurrency:

// WRONG: a reader can slip in between the two lines
cache.delete(key)      // 1. cache is now empty
// <-- concurrent reader misses, reads OLD row, repopulates cache
db.update(key, newVal) // 2. DB now new, but cache holds OLD forever

Why the naive version is wrong: between steps 1 and 2 the DB still holds the old value. A concurrent read misses the empty cache, loads the old row, and writes it back — so the cache is repopulated with stale data that no further event will clear until TTL. Fix the ordering: commit the DB write first, then invalidate (db.update(...); cache.delete(key)), so any repopulating read after the delete can only load the new value. Even this leaves a narrow window; systems that need more use versioned keys or a short "delay-delete" second invalidation.

Purge storms / thundering herd

Purging a hot key sends every concurrent reader to the origin at the same instant (a cache stampede). Prefer refresh (overwrite in place) for hot keys, or add request coalescing / a short lock so only one fetch hits origin.

Ban-list growth

Every read scans the ban list, so its cost is O(list length) per request. Rules for objects nobody re-requests never get to GC, so an unbounded, ever-growing ban list slowly taxes every single read. Cap the list, coalesce overlapping rules, and expire old ones.

Multi-layer invalidation

Browser cache, CDN, and app cache are independent. Purging the CDN does nothing to copies already sitting in users' browsers — those obey the Cache-Control: max-age you sent. Set short/validated TTLs at the edge you cannot purge.

TTL extremes

TTL too long → stale data lingers with no way to force freshness; TTL too short → the cache barely absorbs load and the origin sees near-uncached traffic. TTL is a staleness-vs-load knob, not a fire-and-forget default.

When to use which — and what it costs

The real decision is do you know precisely what changed, and how many keys does it touch?

Choose Purge when the key set is small and known; prefer Ban when it's large or pattern-defined; reach for TTL when you can't observe the writes; layer Stale-While-Revalidate on top when a read must never wait. The main axis to reason about is Purge (eager, exact, expensive to fan out) versus Ban (lazy, pattern-based, cheap to issue but taxes every read).

Takeaways


Sources: Fastly and Varnish documentation on purge vs. ban and lazy ban-list invalidation; MDN and RFC 5861 on Cache-Control, TTL, and stale-while-revalidate; the well-known cache-aside consistency discussion in Facebook's "Scaling Memcache at Facebook" (NSDI 2013) for the invalidate-ordering race. Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Cache Invalidation? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Cache Invalidation** (System Design) and want to truly understand it. Explain Cache Invalidation from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Cache Invalidation** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Cache Invalidation** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Cache Invalidation** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes