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.
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.storedAt4 if age < cell.maxAge:5 return cell.body # FRESH hit, ~2 ms6 if age < cell.maxAge + cell.swr:7 spawn revalidate(url, cell) # async8 return cell.body # STALE, served instantly9 # past TTL+SWR — blocking origin fetch10 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 served3 fresh = origin.fetch(url) # may return v6 or v74 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 edge3 cell = cache.lookup(url)4 cache.evict(url) # next request misses, fetches v75 # without this call, the edge keeps serving the cached6 # copy until storedAt + maxAge + swr — even after7 # 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 count4 if t < deployTimeSec:5 return # served before the fix shipped6 if servedVersion == PRE_DEPLOY_VERSION:7 bugCounter += 1 # user saw the bug post-fix