Loading...
Loading...
Cache-Control headers, surrogate keys, purge vs invalidation at the edge
The last topic absorbed origin heat inside one region with coalesced misses, TTLs jittered across 54–72 seconds, and 5-second per-server local caches, then ended on the remaining threat: every edge point-of-presence missing the same key at once replays the stampede per continent. The edge is not magic, it is just shared cache close to users, a network of point-of-presence (PoP) servers, each a small data center near a city's users, sitting between browsers and your origin server, the master server holding the true files. Your origin says “cacheable for X seconds, keyed by Y, purge by Z.” Get those headers wrong and you either serve stale prices or never get a hit.
Browser → Edge PoP (10ms) → Shield PoP → Origin (80ms)
HIT (cache-control: public, max-age=300) → no origin call
MISS → origin, then store keyed by Host+Path+Vary
Edge key = (host, path, query, Accept-Encoding, Vary header values)Cache-Control: public, max-age=300, s-maxage=600
Surrogate-Key: product-42 category-shoes
Vary: Accept-Encoding
ETag: "a3f5c"
Age: 42max-age, how long the browser keeps it, for browser, s-maxage, how long shared caches keep it, for shared/edge.Surrogate-Key, a tag the origin attaches for bulk purges, tags content for bulk purge.ETag, the version string the origin attaches to each response, lets the edge revalidate without re-downloading.Vary, the header listing which request headers change the response, prevents gzip vs non-gzip mix-up.Edge obeys If-None-Match, the client's stored version sent back for comparison, and Age, how many seconds the object has sat in cache. Use stale-while-revalidate=60, permission to serve a slightly expired copy while fetching fresh in the background, to serve stale while background refresh fills, hides origin latency.
Cache-Control: public, max-age=60,
stale-while-revalidate=30,
stale-if-error=120private on cacheable public assets; edge will not store them.| Strategy | How | When |
|---|---|---|
| Short TTL (60s) | Expire quickly, natural coalescing | Feeds, prices that drift acceptable |
| Surrogate key purge | Origin tags → purges product-42 everywhere in <200ms | Product pages, images |
| Soft purge + background fill | Mark stale, serve stale-while-revalidate | Avoid stampede on popular keys |
Physics sets the budget: light in fiber crosses the Atlantic in ~30ms each way, and real requests with TLS, routing, and server time land near 80–150ms continent-to-continent versus 5–20ms to a local edge PoP. A page assembling twenty origin fetches pays that round trip twenty times in the worst case: two full seconds of geography before a single byte of business logic. Serving the repeatable 80% from the edge collapses most of those trips to local ones, which is why a major edge network's published edge-hit latencies sit an order of magnitude under origin fetches and why every global product eventually buys that shape.
Hit-ratio economics follow directly. At 95% edge-hit ratio, 5% of a million daily requests, 50,000, still reach origin, each paying full latency plus compute. Push to 98% with better key normalization and longer s-maxage on stable content and origin load drops 60% (50k → 20k), often the difference between a comfortably warm origin and one that needs its own scaling story. Shield PoPs (a second edge layer before origin) exist for the long tail: misses coalesce there instead of thundering origin per-PoP.
| Knob | Sane starting value | Why |
|---|---|---|
| s-maxage (edge TTL) | 60–600s feeds, hours–days versioned assets | Longer raises hits linearly and staleness risk with it. |
| stale-while-revalidate | 30–60s | Hides origin latency; too long serves visibly stale pages. |
| stale-if-error | 120–300s, permission to serve stale when origin errors | Origin outage still serves pages; longer masks real downtime. |
| Surrogate-Key cardinality | A few tags per object (product, category) | Hundreds of tags per object slows purges and confuses invalidation. |
Query-string explosion is the edge's version of key explosion: one marketing parameter per campaign times thousands of shared links means the same product page cached under ten thousand keys, each with a near-zero hit rate, while origin absorbs the “cached” traffic. Normalize at the edge, strip or allowlist query params before keying, or the cache reports healthy size and miserable hits while the bill disagrees.
Purge lag is the second classic: surrogate-key purges propagate in hundreds of milliseconds to seconds across PoPs, so a price change shows correctly in one city and stale in another for a beat. Users compare screenshots and both are “right.” Design for it with versioned URLs for embedded assets (no purge needed) and short-TTL-plus-revalidate for HTML shells where a second of mixed generations is tolerable.
Most edge misconfigurations come from one header set applied to everything. Split by mutability instead: hashed static assets get a year at every layer and never need purging; public pages get minutes at the edge with background revalidation hiding the refill; personal content gets no shared caching at all. Three recipes cover the field, and naming the content type first is what keeps teams from serving account pages with asset TTLs.
| Content | Header set |
|---|---|
| Hashed assets (app.a3f5.js) | public, max-age=31536000, immutable |
| Public pages, feeds | public, max-age=60, s-maxage=300, stale-while-revalidate=30 |
| Personal / auth'd | private, no-store (or short max-age, never shared) |
Push everything public and repeatable to the edge with explicit TTLs, versioned assets for a year, pages for minutes with stale-while-revalidate hiding the refill, purges by surrogate key instead of URL lists. The edge trades global freshness simultaneity for global speed, so personal or instant content never gets a shared key no matter how tempting the hit ratio looks. And when the hit ratio looks perfect but users still complain pages feel slow, check time to first byte, the first-byte latency, at the edge versus full page assembly in the browser: a fast shell with twenty uncached API calls or render-blocking third parties, scripts that delay page rendering, still feels slow, and the edge only fixed the shell. Trace what the browser actually waits on, edge caching accelerates delivery, not composition.
Without a shield, every edge PoP that misses the same key fetches from origin independently, a global expiry means dozens of simultaneous origin hits for one logical miss. A shield layer (one mid-tier point-of-presence per region that origin actually talks to) collapses those into a single upstream fetch: edges miss to the shield at local speed, the shield misses to origin once, and request coalescing at both layers absorbs the concurrency. For a product launch with fifty PoPs and a cold key, that is the difference between fifty origin queries and one.
Tiered caching also bounds retry behavior: shield timeouts near 5–10 seconds with a single retry, edge timeouts shorter, so a struggling origin degrades to stale-serving instead of sixty PoPs each retrying three times. Size origin concurrency limits for shield count (a handful of callers), not edge count (hundreds), the shield is an aggregator, and capacity planning that forgets this provisions for the wrong fan-in.
TLS, the encryption between browser and server, termination at the edge is a second instance of the same latency math: session resumption (tickets or shared cache) turns repeat handshakes from two round trips into zero, and keepalive reuse, long-lived reused connections, between edge and origin means the long intercontinental handshake happens once per edge connection, not once per user request. A PoP holding a few hundred warm keepalive connections to origin serves tens of thousands of users without a single new handshake, which is why origin connection limits should be sized for edge count, and why a PoP that churns origin connections under load shows up as origin CPU spikes with no traffic increase to explain them.
Before tagging content, teams purge changed pages by submitting URL lists, because no origin header work is needed. It fails on arithmetic. One product with six image sizes across three locales is eighteen URLs per change, a catalog edit fans out to thousands, and each purge still propagates in hundreds of milliseconds to seconds per point-of-presence, so cities disagree for a beat anyway. One surrogate-key tag purges the whole set everywhere in under 200ms with a single call, and versioned filenames skip purging entirely. Lists work for one page; tags work for a catalog.
Headers right, purges precise, stale served gracefully: everything public and repeatable now rides the edge, with shields collapsing fifty points-of-presence into one upstream fetch and keepalives holding the intercontinental handshake open. But the edge only serves work that can wait and repeat. A checkout must still tell inventory, email, and analytics about every order, exactly once each, even when email is down and analytics wants last month replayed. The open question is where that handoff lives when neither waiting nor repeating will do, in a middleman that either deletes each note on delivery or keeps every page for replay.