Build a CDN (13 scenes)
Scene 06 · Stale-while-revalidate — and the bug it ships
SWR removes the user-visible TTL-boundary wait by serving stale and refreshing in background — and extends any cached bug for the SWR window after deploy.
Previously

Revalidation works but the user still waits one origin RTT on every check — unless we let the edge serve stale immediately and refresh in the background. That directive is stale-while-revalidate.

Scene 06
Stale-while-revalidate — and the bug it ships
Diagram
Top: a single cache cell's lifetime as a horizontal timeline — green FRESH (0..TTL), amber STALE·SWR (TTL..TTL+SWR), gray EVICTED (>TTL+SWR). A 'now' cursor sweeps across; an optional 'deploy fix' marker can sit anywhere on the axis. Below: each user request as a row — left column shows what the user got instantly (v6 or v7), right column shows whether a background revalidation arrow fired to origin. Top-right: a bug counter for users served the pre-deploy version after deploy. **stale-while-revalidate (SWR)** — directive that lets the edge serve the stale copy for N seconds past TTL while refreshing in the background, removing the user-visible wait. **stale-if-error** — companion directive that lets the edge keep serving stale when origin returns 5xx, trading freshness for survival during an origin incident.
cache cell · /index.htmlstored: v6SWR ONusers who saw the bugserved pre-deploy after deploy0CELL LIFETIMEFRESH0..60sSTALE · SWR60s..660sEVICTED>660st=0t=TTL (60s)t=TTL+SWR (660s)now · t=0.0sRECENT REQUESTSuser-visible responseinstantbackground revalidationasync → originno requests yet
/index.html · max-age=60, stale-while-revalidate=600. Watch the cursor cross the TTL boundary.
in FRESH — every user gets v6 instantly from the edge.
The 'now' cursor sweeps from t=0 across the cell's lifetime. While it's in the green FRESH band, the cell serves v6 instantly. The moment it crosses TTL into the amber STALE·SWR band, the edge KEEPS serving v6 instantly — and a background arrow fires to origin to refresh. When that lands, the cell flips to v7 and subsequent users see the new version. User-visible latency stays ~2 ms across the boundary.
Implementation
Edge.handleRequest
the three SWR branches: fresh, stale-but-in-SWR, evicted
1def handleRequest(url):
2 cell = cache.lookup(url)
3 age = now() - cell.storedAt
4 if age < cell.maxAge:
5 return cell.body # FRESH hit, ~2 ms
6 if age < cell.maxAge + cell.swr:
7 spawn revalidate(url, cell) # async
8 return cell.body # STALE, served instantly
9 # past TTL+SWR — blocking origin fetch
10 fresh = origin.fetch(url)
11 cache.store(url, fresh)
12 return fresh.body
Edge.revalidate
background refresh fired by the SWR branch; user already got bytes
1def revalidate(url, oldCell):
2 # runs in the background; user has already been served
3 fresh = origin.fetch(url) # may return v6 or v7
4 cache.store(url, {
5 body: fresh.body,
6 storedAt: now(),
7 maxAge: fresh.cacheControl.maxAge,
8 swr: fresh.cacheControl.swr,
9 })
10 # next request lands in the new FRESH window
Operator.purgeUrl
the only way to short-circuit SWR before the window closes
1def purgeUrl(url):
2 # explicit invalidation at the edge
3 cell = cache.lookup(url)
4 cache.evict(url) # next request misses, fetches v7
5 # without this call, the edge keeps serving the cached
6 # copy until storedAt + maxAge + swr — even after
7 # origin has been deploying the fix the whole time
Observability.recordServe
counts users served the pre-deploy version after the deploy mark
1def recordServe(servedVersion, t):
2 if deployTimeSec is None:
3 return # no deploy yet; nothing to count
4 if t < deployTimeSec:
5 return # served before the fix shipped
6 if servedVersion == PRE_DEPLOY_VERSION:
7 bugCounter += 1 # user saw the bug post-fix