Skip to main content

CDN Invalidation & Edge Strategies

The Lie Is in Four Hundred Cities — and a Million Pockets

The sneaker price drops again — 220to220 to 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.

A system-design infographic titled 'You can purge four hundred cities. You cannot purge a pocket.' showing the three weapons of CDN invalidation across the two cache layers. The top band draws the delivery chain: an origin holding the fresh price of one hundred eighty dollars, three edge PoPs, and under them users' browsers - with a bracket over the PoPs labeled purgeable, one API call, and a bracket over the browsers labeled unpurgeable, no API reaches a phone. The middle band shows the one header with two dials: Cache-Control max-age sixty for the browsers, a short leash for the layer you cannot purge, and s-maxage one day for the edge, a long hold for the layer you can purge on demand, with the rule TTL length proportional to your ability to invalidate. The bottom band lays out the three weapons as flows: TTL, wait it out, staleness bounded by the clock; PURGE, an API call that sweeps the edge in about one hundred fifty milliseconds via surrogate keys like product-8841 clearing every page carrying the tag, but explicitly not reaching browsers, and warning of the post-purge stampede softened by soft purge; and VERSIONED URLS, where app dot a3f92b dot js becomes a new filename on every deploy so every layer, browsers included, simply fetches the new URL - never invalidate, cache for a year, immutable - with the split strategy: HTML short-lived and purgeable, fingerprinted assets cached forever. The palette is dark slate with an amber origin, sky edge PoPs, red stale browsers, and emerald versioned-URL flow.

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 overrides max-age at 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.

How a CDN purge works and what it cannot reach. A single command that tags a surrogate key fans out to every edge point of presence and clears each tagged copy in roughly 150 milliseconds worldwide, so the entire edge is cleared. But it reaches no browser by design: no purge API can reach a phone, so a browser's private cache holds the old copy until its own TTL expires. And a purge must be soft: a hard purge empties 400 edge caches at once and they all refetch the origin together, so you serve stale while the edge refills to avoid stampeding your own origin.

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 220.Thenthepricedropsto220**. Then the price drops to **180 — and every box still holding the old copy turns red: it is serving the lie.

Do this in order:

  1. Start on LONG-TTL (the naive setup: an hour of max-age for 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.
  2. 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.
  3. 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.

Purge the Planet. An origin, three edge PoPs, and four browsers that have all cached your $220 price - then the price drops to $180 at the origin, and every box holding the old copy turns red: it is serving the lie. Pick your weapon and watch who heals. Under LONG-TTL the purge button sweeps the edge clean in a blink - and the browsers stay red for up to an hour, because no API on earth reaches into a user's pocket. Under the SPLIT (short browser TTL, long edge TTL) the same purge still only heals the edge, but the browsers' short leash means the lie dies within sixty seconds - staleness bounded by design, not hope. Under VERSIONED URLS the deploy changes the filename itself, and every layer - browsers included - simply fetches the new URL on its next request: nobody needs purging, and the purge button greys out to make the point. Live counters keep score: how many users are seeing the lie, how long until everyone sees the truth, and how many purge API calls you spent. The one lesson prose cannot deliver: watch the browsers stay red after a 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-livedno-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 versioned or fingerprinted URLs invalidate every cache layer, including the browser purging cannot reach. On each deploy the filename changes, for example app.a3f92b.js becoming app.9d41c7.js, so a new name is a new object. Because the URL is new, every layer fetches it fresh — the edge, the browser in a pocket, and any proxy or ISP cache — reaching the pocket a purge cannot. The rule that makes it safe is a split: fingerprinted assets are immutable and cached a year, while the short-lived HTML that names them changes on deploy, so one deploy flips the whole page to the new assets at once.

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, long s-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-age runs 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=86400 everywhere — great hit rate!" You just promised the planet's browsers a day-long license to lie, revocable by nobody. Long lifetimes belong behind s-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.
  • "immutable on everything, then!" Only on URLs whose content genuinely can never change — i.e., fingerprinted ones. Stamp it on /index.html and 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-error says 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-age keeps browsers on a short leash, s-maxage lets 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 immutable for 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-revalidate kills the expiry cliff, stale-if-error keeps 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.