CDN Invalidation & Edge Strategies
The Lie Is in Four Hundred Cities — and a Million Pockets
The sneaker price drops again — 180. Two chapters ago this was a solved problem: delete one Redis key, done. Last lesson made it planetary: the old price is now cached in PoPs across four hundred cities. Fine — CDNs have a purge API. One call, the edge forgets, next reads pull $180. Crisis over?
Not quite. Because between your edge and your users' eyeballs sits one more cache — and it's the one nobody remembers until it burns them. The browser. Every user who loaded that product page is carrying a private copy of it on their own device, and that copy obeys whatever lifetime you stamped on it when you served it. If you said "cache this for a day," then for up to a day, those users see $220 — and there is no API on Earth that reaches into a pocket. You can purge four hundred cities in a heartbeat. You cannot purge a single phone.
That asymmetry — one layer you command, one layer you only influence — is the whole shape of this lesson. Around it hang the three weapons every real system mixes: TTLs (decide up front how long a lie may live), purges (rip content out of the edge on demand), and the quiet champion, versioned URLs (arrange things so nothing ever needs invalidating at all). By the end you'll know exactly which weapon aims at which layer — and why the best teams fire the purge API far less often than you'd think.
Scope: this is the freshness playbook for the caches between you and the user — the edge you rent and the browsers you don't. What a CDN is was last lesson; the races inside a single cache were the invalidation lesson before it. Both get reused, not retaught.

Two Layers, One Header
Both layers take orders from the same place: one HTTP response header, Cache-Control, stamped on everything your origin serves. It has two dials that matter here — and the difference between them is the two-layer strategy.
max-age=N— "this response is fresh for N seconds." Every cache honors it, and it's the only lifetime browsers listen to.s-maxage=N— the s is for shared. Only shared caches — your CDN's PoPs — obey it, and where present it overridesmax-ageat the edge. Browsers ignore it completely.
Two dials, two layers. So ask the strategic question: how long should each layer be allowed to remember? And the answer falls out of the asymmetry from the hook: give each cache a lifetime proportional to your power to correct it. The edge you can purge in a heartbeat — so let it hold content for a day; if the data changes, you'll reach in and fix it. The browser you can never correct — so keep it on a short leash: sixty seconds, maybe less. Written down, the canonical split looks like this:
Cache-Control: public, max-age=60, s-maxage=86400
Browsers stay at most one minute wrong — bounded by design, not hope. The edge holds the copy for twenty-four hours, soaking up the traffic and shielding your origin — safely, because it answers to your purge API. One header line, and you've priced the staleness of each layer according to what it costs you to fix.
Notice what you're really doing: accepting a bounded lie at the layer you can't control, and buying maximum offload at the layer you can. Every strategy in this lesson is some rearrangement of that trade.
Purge: Deleting Something from Four Hundred Cities
For the layer you do command, the tool is purge: an API call that tells every PoP on the planet to forget an object, right now.
The basic form takes a URL — "forget /product/8841, everywhere." But real invalidations are rarely one URL. That price appears on the product page, six category listings, the seller's storefront, the sale landing page, cached search results… which URLs contain this price? is a question you usually can't answer. So serious CDNs let you attack it from the other side: surrogate keys (cache tags). When your origin renders a page, it stamps the response with tags naming everything on it — Surrogate-Key: product-8841 seller-312 summer-sale — and the edge indexes copies by tag. Now "purge tag product-8841" evicts every page that contains that product, across every PoP, in one call — no URL list required. (Recognize the idea? It's the grouped-invalidation trick from the cache-invalidation lesson — versioned keys' cousin — wearing an HTTP header.)
Two operational facts decide how purge feels in practice:
- Speed is a solved problem — if your CDN wants it to be. Fastly has purged globally in ~150 milliseconds since 2011 — no central commander, just PoPs gossiping the purge to each other in an exponential cascade (their bimodal multicast). Blink and the planet has forgotten.
- Cost and speed vary wildly by vendor. CloudFront invalidations take seconds to minutes and are metered: 1,000 free paths a month, then $0.005 per path. A team that "just invalidates everything" on each of 40 daily deploys can burn tens of thousands of dollars a year on what Fastly gives away in 150 ms. When purge is slow or priced, that's the platform whispering: stop leaning on purge — design it out — which is exactly where this lesson is headed.
And keep the hook's asymmetry in view: everything purge touches is the shared layer. The browsers that cached the old page are untouched, counting down their max-age in blissful ignorance. Purge is a scalpel for the edge — it has no reach into pockets. That's not a flaw to fix; it's a fact to design around, and you already did: the short browser leash from the previous section is precisely the containment for the layer purge can't reach.

Purge the Planet
Time to feel the asymmetry instead of reading about it. Below: your origin, three edge PoPs, and four browsers that have all cached the product page at 180 — and every box still holding the old copy turns red: it is serving the lie.
Do this in order:
- Start on LONG-TTL (the naive setup: an hour of
max-agefor everyone). Change the price, then hammer the ⚡ purge button. Watch closely: the PoPs snap green in a blink… and the browsers stay red. Nothing you do reaches them. That red is a user staring at the wrong price for up to an hour, and your purge API is powerless — you cannot purge a pocket. - Switch to the SPLIT (
max-age=60, s-maxage=1 day) and repeat. The purge still heals only the edge — but now the browsers' short leash means every red box flips green within sixty seconds. Staleness didn't disappear; it became bounded, by design. This is why that header split exists. - Switch to VERSIONED URL and deploy the change. No purge — the filename itself changed, so every layer, browsers included, simply fetches the new URL on its next request. Watch the purge button grey out: nothing needs purging when nothing is ever invalid.
Keep score with the counters — users seeing the lie, time until everyone sees the truth, and purge calls spent — and try the fast-forward to watch the TTL clocks actually run out. The one moment to burn into memory: the browsers staying red after a completely successful purge.

The Purge That Stampedes You
One more edge (pun intended) on the purge knife, and it's one you're already trained to see. Purge a hot object — or worse, run a panic purge-all — and think about the very next second: every request for that object, at every PoP, is suddenly a miss. If your hit ratio was 98%, the origin that normally sees 2% of traffic can be hit with up to fifty times its usual load, instantly. You didn't get attacked; you pressed the button yourself. It's the thundering herd from the stampede lesson — summoned voluntarily, at planetary scale.
The cures are the stampede playbook with CDN names on it:
- Soft purge. Instead of deleting the object, mark it stale. The edge keeps serving the old copy for a few extra seconds while it revalidates in the background — users never wait, and the origin refills at its own pace instead of under a landslide. It's stale-while-revalidate, triggered on demand — and it should be your default purge mode; save hard purges for content that's genuinely dangerous to show (a legal takedown, a leaked price), not merely outdated.
- The origin shield (from last lesson) — whatever refill storm does happen gets collapsed to a single origin fetch on its way through the hierarchy.
- Shield priming — the neat production trick: the purge service itself immediately re-fetches the purged URL once, through the shield, re-warming the cache before user traffic discovers the hole. The herd arrives to find the buffet already restocked.
The meta-lesson: invalidation and stampedes are the same subject at this scale. Every act of forgetting is a miss storm you're scheduling; the difference between a clean purge and an outage is whether you scheduled it politely.
Versioned URLs: The Weapon That Reaches Pockets
Now the strategy that makes most purging unnecessary — the one the interactive saved for last, and the quiet workhorse of the modern web.
Your build tool (webpack, Vite — any of them) hashes each file's content into its filename: app.js becomes app.a3f92bc1.js. Change one character of the source and the hash changes, so the URL changes. And that flips the entire invalidation problem on its head: you never need to make any cache forget the old file, because nobody will ever ask for it again. The new HTML references the new name; the old copy — in every PoP, in every browser, in every pocket — is instantly irrelevant and quietly ages out. You didn't invalidate the planet. You made the planet's copies obsolete. Notice this is the third time this course has handed you the same trick — versioned cache keys, then the version-bump from distributed caching, now at the URL level — because it's the same deep idea: don't fix stale copies; make them unreachable.
Since a fingerprinted URL's content can never change, you can cache it with maximal aggression:
Cache-Control: public, max-age=31536000, immutable
One year, and immutable tells browsers "don't even revalidate this on refresh." Perfect caching at every layer — including the unpurgeable one — with zero invalidation, ever.
There's one catch, and it dictates the architecture of basically every production website: something has to point at the new filenames. That something is the HTML. So the HTML itself must stay short-lived — no-cache or a tiny max-age — so it always references the current build. Which yields the split you'll find at every serious shop:
- HTML → short TTL, purgeable, event-purged on change — the small, cheap, always-fresh pointer.
- Everything the HTML points at → fingerprinted,
immutable, cached for a year — the heavy, never-invalidated payload.
The pointer is tiny and easy to keep honest; the payload is huge and never needs to be. That division of labor is why the web's asset problem is, for practical purposes, solved.

How Real Sites Wire It
Put the weapons together and you get the standard production loop — worth seeing end-to-end once.
The publish flow. An editor fixes a breaking-news headline; a merchant drops a price. The CMS or catalog emits an event on save — and a webhook fires a surrogate-key purge: purge tag article-456. Within ~150 ms, every page carrying that article — the story page, the homepage, the section fronts — is stale at every PoP; soft purge means readers keep getting instant responses while the edges quietly refill. Total publish-to-planet latency: about a second. No URL lists, no waiting for TTLs, no herd. That's event-driven invalidation — the CDC idea from the invalidation lesson, speaking HTTP.
The safety nets. Two more Cache-Control extensions round out the kit, both of which you've met in spirit:
stale-while-revalidate=600— after expiry, serve the stale copy instantly and refresh in the background. No user ever waits on a revalidation; expiry stops being a cliff (this was the herd-armor from the stampede lesson — here it's a header any CDN honors).stale-if-error=86400— the underrated one: if the origin is down or erroring, keep serving the last good copy for up to a day. Your origin can be on fire and users keep reading yesterday's page instead of a 500. The CDN as your backup of last resort — availability for one header's worth of effort.
The decision map, condensed:
- Static assets → fingerprint +
immutable+ a year. Never purge; nothing to purge. - HTML and data-driven pages → short
max-age, longs-maxage, surrogate-key soft-purge on change, plus both stale-* nets. - Real emergencies (takedowns, dangerous content) → hard purge by tag, eyes open to the herd.
And the rule that generates all three rows: the length of a cache's TTL should match your power to correct it. Browsers: short. Edge: long, because purge. Fingerprinted assets: forever, because never wrong.
Traps That Sound Right
- "We purged the CDN — everyone sees the new version now." Everyone at the edge. Browsers keep their copies until
max-ageruns out, and no API reaches them. If your browser TTL was a day, some users see the old page for a day. Purge commands the edge; only TTL design bounds the pocket. - "Just set
max-age=86400everywhere — great hit rate!" You just promised the planet's browsers a day-long license to lie, revocable by nobody. Long lifetimes belong behinds-maxage, where your purge API can reach. - "Cache invalidation is hard, so purge everything on every deploy." That's a scheduled thundering herd (and on metered CDNs, a real bill). Fingerprint your assets and the deploy needs no asset purging at all — the URL change does it for free.
- "
immutableon everything, then!" Only on URLs whose content genuinely can never change — i.e., fingerprinted ones. Stamp it on/index.htmland you've made your pointer permanently stale. The split exists precisely because the two kinds of content need opposite treatment. - "Hard purge is the safe choice — stale is bad." Backwards for most content: hard purge of hot objects is how you stampede yourself. Soft purge keeps users fast and origins calm; save the hard hammer for content that must vanish now.
- "The origin is down — nothing we can do."
stale-if-errorsays otherwise: the edge can carry your whole read path on last-known-good copies while you fix things. Not configuring it is leaving free availability on the table.
Key Takeaways & What's Next
- Two layers, one asymmetry: the edge is purgeable (an API call away); the browser is unpurgeable (no API reaches a pocket). Every sane strategy starts from that fact:
max-agekeeps browsers on a short leash,s-maxagelets the edge hold long — TTL length ∝ your power to correct that layer. - Purge is a scalpel for the edge: by URL for one-offs, by surrogate key for "everything containing X," soft by default (stale-bridge, no herd), hard only for must-vanish-now. Global purge can be ~150 ms — or slow and metered, depending on vendor. And purging a hot object is a self-scheduled stampede: soft purge + origin shield + priming are the manners.
- Versioned URLs delete the problem: content-hash the filename, serve it
immutablefor a year, and stale copies become unreachable instead of wrong — the only weapon that instantly reaches browsers. The universal split: HTML short and purgeable; fingerprinted assets forever. - Wire it event-driven (publish → tag purge → planet fresh in ~a second) and string the safety nets:
stale-while-revalidatekills the expiry cliff,stale-if-errorkeeps you up while the origin is down.
And with that, the caching chapter's ideas are complete — from one RAM lookup to a planetary hierarchy, and you'll notice the same five moves repeating at every scale: bound the lie, collapse the herd, version instead of fix, shield the truth, and measure hit rate above all. Next: hands on keyboard — the Redis codelab — where you'll cache a genuinely slow endpoint yourself and watch every number from this chapter show up in your own terminal.