Networking
Part 6 of 6 · CDN & edge cacheRequest Collapsing / Coalescing
When N waiters miss the same key, one fetch goes upstream. Collapsing is local; origin shield is topological. You still need single-flight at every tier or a popular expiry looks like a DDoS.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Overview
Caching stores a completed response. Collapsing deduplicates in-flight work before a response exists. When a hot key expires, hundreds of clients miss together. Without collapsing, each miss is an origin (or DB, or payment API) call — a thundering herd. With collapsing, waiters park on one leader.
It is not a cache: after the leader finishes, later requests HIT the store. It is not a shield: 200 POPs with perfect local collapsing still send 200 origin fetches unless a shared tier sits in front.
Why single-flight wins
| Aspect | Independent retries | Rate limit + retry | Single-flight (winner locally) | Shield + collapse |
|---|---|---|---|---|
| Concurrent misses | N upstream calls | Bounded, still bursty | 1 call per key | 1 call per key across POPs |
| Tail latency | N times origin | Backoff adds delay | Origin latency + wait-queue | Same, then HIT |
| Failure | Independent | Independent | One error fans out to all waiters | Edge can still serve SIE stale |
| Complexity | None | Moderate | Registry + waiters | Extra hop, vendor feature |
- 1
TTL expiry → N origin fetches on one node
No collapsing
Fifty parallel misses on
/hotbecome fifty origin GETs. CPU, DB, and third-party QPS spike together. Serverless cold starts multiply. - 2
Winner locally: in-flight map keyed like the cache
First waiter is the leader. Everyone else awaits the same promise. Success or error broadcasts. nginx proxy_cache_lock and Go singleflight are this idea.
- ?
Winner globally: collapse at edge, mid-tier, and shield
Local single-flight times POP count is still a herd. An origin shield (Cloud CDN, CloudFront, Fastly) is the topology choke point. Skip collapsing on uncacheable private traffic or you serialize users behind one slow POST.
Core mechanics
- Key derivation — same identity as the cache key: canonical URL plus allow-listed Vary. Collapse too broadly (ignore
Authorization) and you merge distinct users. Collapse too narrowly (Vary: Cookie) and nobody shares a flight. See cache keys. - Dedup window — usually "while in flight," not a timed bucket. A timed window that is too long inflates waiter latency; too short and you miss the herd.
- In-flight registry — concurrent map of key → promise/future. Go's
singleflightis the canonical library. - Wait queue — other callers await that promise. Bound the queue or memory explodes on a stuck origin.
- Fan-out — complete once; every waiter gets the same bytes or the same error.
- Cleanup — delete the registry entry in
finallyso the next miss can lead again.
nginx proxy_cache_lock holds a lock per cache key so only one request updates the cache; others wait. Redis EVAL is how app-tier caches implement compare-and-set single-flight (set inflight:key atomically, losers wait or retry). Same pattern, different layer.
Sequence
- 1
Client A → Collapser
GET key
- 2
Collapser → Collapser
Step1 inflight miss
- 3
Collapser → Origin
Step2 leader fetch
- 4
Client B → Collapser
GET same key
- 5
Collapser → Collapser
Step3 inflight hit
- 6
Collapser → Collapser
Step4 enqueue B
- 7
Origin → Collapser
response
- 8
Collapser → Collapser
Step5 resolve waiters
- 9
Collapser → Client A
body
- 10
Collapser → Client B
body
Lesson map
Request Collapsing / Coalescing
When N waiters miss the same key, one fetch goes upstream. Collapsing is local; origin shield is topological. You still need single-flight at every tier or a popular expiry looks like a DDoS.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB a["Client A"] b["Client B"] s["Collapser"] o["Origin"] a -->|GET key| s s -->|Step2 leader| o b -->|GET same key| s o -->|response| s s -->|body| a s -->|body| b
Collapsing vs shielding vs SWR
| Tool | What it collapses | Where |
|---|---|---|
| Single-flight | Concurrent misses for one key | One process / one cache node |
| Origin shield | Misses across POPs | Topology in front of origin |
| SWR | User-visible wait on revalidation | Serves stale while one refresh runs |
SWR without a leader flag still stampedes: every stale request might start a refresh. Soft purge plus SWR plus collapsing is the humane publish path. Hard purge-all plus no collapsing is the incident.
TLS HTTP/2 multiplexing reuses connections; it does not merge identical GETs. Do not say "we enabled HTTP/2 so we do not need collapsing."
Naming collision: connection coalescing. Browsers also "coalesce" HTTP/2 connections when certificates and origins allow (one socket to a CDN VIP serving many hostnames). That is not request collapsing. Connection coalescing saves handshakes. Request collapsing saves upstream GETs for one cache key. Say which one you mean.
Worked counts
Assume one hot HTML object expires everywhere at once. No SWR (worst case). Each POP has many clients in the same millisecond.
| Setup | Origin GETs |
|---|---|
| 50 clients, 1 node, no collapse | 50 |
| 50 clients, 1 node, single-flight | 1 |
| 50 clients × 200 POPs, collapse per POP, no shield | 200 |
| Same, plus shield that also single-flights | 1 |
| Same, plus SWR so most clients take stale | 0 or 1 background refresh |
Those five numbers are the interview. If your collapse key includes a session cookie, you fall from the "1" rows back to the "50" row even on one node.
Cold start / serverless. The leader pays the cold-start; waiters reuse the warm result. That is a latency win only if you bound how long waiters will sit. A 30s origin timeout × 10k waiters is a memory incident.
Rate-limit protection. Third-party APIs (maps, payments, auth) often cap QPS. Collapsing at the edge of your service is how 10k page views do not become 10k vendor calls for the same public config payload. Cache the result too; collapsing alone only helps the simultaneous window.
Idempotency. Collapse GET and HEAD. Do not collapse POST, PUT, PATCH, or DELETE unless you have a dedicated idempotency-key design — that is a different lesson. A coalesced POST would apply a mutation once for N callers who each expected their own side effect. The wait-queue is for reads.
Failure modes
Error fan-out. The leader's 503 is everyone's 503. Decide: propagate, retry once (only if idempotent GET), or fall back to SIE stale. A retry storm from all waiters undoes collapsing — one retry belongs to the leader.
Uncacheable leader. If the leader's response is Cache-Control: private, no-store, do not store it, and think twice before collapsing it. A slow personalized request should not block fifty other users who happened to share a too-fat key.
Stuck origin. Waiters pile up until timeout. Set a lock TTL (proxy_cache_lock_timeout in nginx). Shed excess waiters with 503 rather than unbounded memory.
Stampede after success. The herd is collapsed during the miss. After fill, traffic is HITs. The next expiry needs jittered TTLs or SWR so the herd does not reform on a timer.
Global lock. One mutex around the whole registry serializes unrelated keys. Stripe by key (Go singleflight does).
Deep dive · App-tier single-flight and Redis
CDNs are not the only place. A process-local singleflight protects a single replica. Multiple app replicas still stampede Redis/DB unless you also collapse there: SET NX an inflight flag with EXPIRE, or a lock via EVAL. That is the same state machine as the playground, with a distributed map. Do not wait on Redis with an unbounded client queue. Combine with cache-aside TTL jitter so replicas do not expire in lockstep.
Playground: 50 waiters, then 200 POPs
In-memory maps. No fetch. First loop is one node. Second loop is many nodes without a shield, then with a shield that has its own single-flight.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Teaching numbers: 50 waiters → 1 origin on one node; 3 POPs × 1 leader → 3 origin without a shield; with a shield → 1 origin. Error path stores nothing, so the next wave can lead again.
Interview Q&A
What is request collapsing and when do you use it?
Answer
Merge concurrent identical misses into one upstream call and fan the result out. Use it on CDN nodes, API gateways, and app-tier cache-aside whenever a hot key can expire or miss. It cuts origin QPS, third-party rate-limit burn, and tail latency from stampedes.
How is collapsing different from caching?
Answer
Caching reuses a finished response. Collapsing coalesces in-flight work when there is not yet a stored body. After the leader completes, caching takes over. Origin shield combines both: HIT at the shield, and collapse of shield misses.
What is single-flight? Name a library.
Answer
A registry of in-flight calls keyed by string, wait-queue, fan-out, delete on complete. Go golang.org/x/sync/singleflight is the standard citation. nginx proxy_cache_lock is the reverse-proxy form.
Failure-mode when many callers share one upstream request?
Answer
The leader's error, timeout, or wrong variant is everyone's. Retry only in the leader, and only for idempotent GETs. Prefer SIE stale over a synchronized 502. Bound wait time. Do not collapse non-idempotent POST.
How do you bound memory of the in-flight registry?
Answer
One entry per in-flight key, deleted in finally. Cap waiters per key; reject extras. Lock timeout so a dead origin cannot pin entries forever. Do not use a process-global mutex that grows a giant waiter list across unrelated keys.
How does an origin shield work with collapsing?
Answer
Each edge collapses locally. Remaining leaders go to the shield. The shield collapses those leaders into one origin fetch and caches the result. Across 200 POPs you want ~1 origin GET per object, not 200. Cloud CDN origin shield is this topology.
What goes in the collapse key?
Answer
The cache key. If you omit a Vary axis that changes bytes, waiters share the wrong body. If you include session cookies, collapsing never fires. Fat keys defeat coalescing the same way they defeat HIT ratio.
Uncacheable leader — what goes wrong?
Answer
A private or slow response becomes the flight everyone waits on, then is not stored, so the next millisecond starts a new herd. Do not collapse Authorization traffic into the public key. Time out the lock. Skip collapsing for methods that are not GET/HEAD.
Does SWR replace collapsing?
Answer
No. SWR lets you serve stale during refresh. Collapsing ensures one refresh. Together they hide origin and protect it. SWR without a revalidating flag is still a herd of background fetches.
HTTP/2 multiplexing vs collapsing?
Answer
HTTP/2 (and HTTP/3) put many streams on one connection — a TLS / Anycast win for handshakes. Collapsing merges identical requests so you do not send many streams for one key. Enable both.
What happens after a mass hard purge?
Answer
Every edge MISS at once. Without shield + collapsing + soft purge, origin QPS equals popular-URL × POP count × clients. Prefer soft purge so most clients take stale while leaders refill. That is the purge page plus this page on one whiteboard.
Where should collapsing live besides the CDN?
Answer
Any fan-in in front of a slow or expensive dependency: API gateway, BFF, app replica, Redis cache-aside. Process-local singleflight is the first line; a distributed lock or SET NX covers multiple replicas. CDN collapsing does not protect your DB from your own origin servers if each origin replica stampedes the database on a miss.
Pitfalls
| Pitfall | Mitigation |
|---|---|
| Unbounded wait queue | Max waiters, lock timeout, 503 shed |
| Collapse key too fat or too thin | Same allow-list as cache keys; never raw Cookie; never drop auth when bytes differ |
| Error fan-out retry storm | Leader-only retry; SIE stale |
| Global mutex | Per-key registry (singleflight) |
| Dedup window too large | In-flight lifetime only; measure waiter wait |
| Collapse then timer stampede | TTL jitter, SWR, shield |
| Collapsing POST / private | GET/HEAD public keys only |
| Collapsing only at the edge | Still 1 origin per POP; add a shield |
50 clients, 1 node, 1 hot key, simultaneous miss: origin GETs with and without collapsing. Then 200 POPs. Then 200 POPs plus a shield that also single-flights. Write 50 / 1 / 200 / 1 on the board. If your numbers disagree, your collapse key is not shared.
Go Deeper
- Go — singleflight
- ACM Queue — The thundering herd
- Redis — EVAL
- Google Cloud CDN — Origin shield
- nginx — proxy_cache_lock
Sibling pages: CDN hierarchy · Cache keys · SWR / stale-if-error · Purge and generation tokens · TLS / Anycast