Purge — URL, surrogate key, or sledgehammer

Purge is a write propagated to every POP; surrogate keys let one API call invalidate every related object atomically — without them you are either listing every URL by hand or nuking the whole cache.

Previously

Purge is the escape hatch you reached for at the end of the last scene — but purging a copy in one POP doesn't help the other 299 POPs still serving it, so the question becomes WHICH POPs and WHICH entries one purge call actually invalidates.

Scene 07

Purge — URL, surrogate key, or sledgehammer

  1. Watch
  2. Try it
  3. Predict
  4. Capture
purge propagation · 12 POPsSURROGATE-KEY TAGSproduct:42homepagecatalogotherEDGE POPsus-west8/8us-east8/8us-centr…8/8sa-east8/8eu-west8/8eu-centr…8/8eu-north8/8me-centr…8/8af-south8/8ap-south8/8ap-south…8/8ap-east8/8PURGE CONTROLSURL or surrogate key/logo.pngURL purgeSurrogate-key purgeZone-wide purgePROPAGATION TIMELINEp50 · 150 msp99 · 320 msingressall POPsORIGIN RPSedge → originbaseline · 8ceiling · 1.2kNOW8steady-state · cache hits absorb loadACTIVE PURGEidle — pick a purge kind below
12 POPs, each with its own cached cells. Watch the purge wave radiate from the ingress POP.
What to watch for

A URL purge for /logo.png is fired from the Frankfurt ingress POP. Watch the wave ripple outward across all 12 POPs — only the /logo.png cell clears in each one. Origin RPS stays flat: each POP's next request will be a single miss.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Edge.purge_handlers
three flavors — same control plane, different match rules
def purge_url(url):
p = { kind: 'url', target: url }
broadcast_purge(p) # one cell per POP
def purge_surrogate_key(tag):
p = { kind: 'surrogate', target: tag }
broadcast_purge(p) # every cell sharing tag
def purge_zone():
p = { kind: 'zone', target: '*' }
broadcast_purge(p) # every cell, every POP
POP.apply_purge
what each POP does when the purge message arrives
def apply_purge(p):
for cell in self.cache:
if matches(cell, p):
cell.invalidate() # next request = miss
ack(p.id, self.pop_id)
def matches(cell, p):
if p.kind == 'zone': return True
if p.kind == 'url': return cell.url == p.target
if p.kind == 'surrogate': return p.target in cell.tags
Control.broadcast_purge
gossip the purge to every POP; track per-POP completion
def broadcast_purge(p):
p.id = new_purge_id()
pending = set(all_pops)
for pop in all_pops:
pop.send(p) # bimodal multicast / gossip
while pending:
pop_id = wait_for_ack(p.id)
pending.remove(pop_id)
# p50 ~150 ms; p99 = slowest POP in the fleet
return { p50: ack_p50(), p99: ack_p99() }

Where this sits in Build a CDN

Scene 07 of 13, in the Control act — Purge flavors, the cache key, and the Vary footgun.. Three purge flavors: URL (precise, slow at scale), surrogate-key (atomic, fast), zone-wide (sledgehammer that stampedes origin).

Up next. Purge invalidates entries — but what determines which entries are matches in the first place? The cache key, and the Vary header that quietly multiplies it.

All 13 scenes in Build a CDN · Every curriculum

Built with Arqly
Every scene in Build a CDN builds on the one before it.All 13 Build a CDN scenes