Content delivery networks (CDN)
A CDN caches content at edge nodes near users, cutting latency and offloading the origin. Levers: cache-control/TTL, hit ratio, push vs pull, origin shielding, purge. Knowing what NOT to cache, and how invalidation works, beats a stale-data incident.
A product team shipped a “small” pricing change and pushed it live. Customers in Europe kept seeing the old prices for the rest of the day; customers in Asia for two days. Nobody had deployed a bug — the new price page carried Cache-Control: max-age=86400, and hundreds of CDN edge nodes around the world were each serving a perfectly cached copy that was now wrong, with no instruction to recheck. The same edge cache that made the site fast for everyone had pinned a stale price in front of every customer, and the team learned the hard way that on a CDN, putting something at the edge is easy and taking it back is the hard part. The lesson wasn’t “don’t use a CDN” — it was that TTLs and purge are a deliberate design decision, not a default to ignore.
What a CDN buys you
A content delivery network is a globally distributed fleet of cache servers (edge nodes or PoPs — points of presence) placed close to users. When a request arrives, the nearest edge node either serves a cached copy (a cache hit) or fetches from your origin, caches it, and serves it (a cache miss). It buys two things that compound:
- Lower latency. The user talks to an edge node a few milliseconds away instead of an origin that might be a cross-region trip of ~150 ms (the latency hierarchy from the scalability unit). Physics, not cleverness: shorter distance, fewer round trips.
- Origin offload. Every hit is a request your origin never sees. A site at a 95% hit ratio sends only 1 in 20 requests to the origin, so the origin fleet can be ~20× smaller — and survives traffic spikes that would otherwise melt it.
It also absorbs volumetric attacks and smooths bursts, because the edge fleet is enormous and the origin hides behind it. The networking track covers how a request reaches the edge (DNS/anycast routing, the edge’s own CDN lesson); here we stay at composition altitude — what the CDN tier does in your architecture and how you control it.
Push vs pull CDNs
There are two ways content gets onto the edge.
A pull CDN is lazy and demand-driven: the edge holds nothing until a user requests it. The first request for an object is a miss — the edge pulls it from the origin, caches it, and subsequent requests are hits until the TTL expires. This is the default and the right choice for most sites: you don’t pre-upload anything, the cache naturally holds only what’s actually requested, and rarely-accessed content simply isn’t stored. The cost is that the first user per object per edge pays the miss latency.
A push CDN is eager: you upload content to the CDN ahead of time (or on publish), and it’s distributed to edges before anyone asks. This suits large, predictable assets — a software release, a video library, a big launch you know will be hammered — where you don’t want the first user eating a miss on a multi-gigabyte file and you’d rather control exactly when content propagates. The cost is that you manage what’s on the edge, and you store everything whether it’s requested or not.
The rule of thumb: pull for general web traffic (lazy, self-managing), push for large predictable files where you want to pre-warm the edge.
Cache-Control, TTL, and the hit ratio that pays for it all
The CDN doesn’t guess how long to cache — you tell it, with HTTP cache headers. Cache-Control: max-age=3600 says “this is fresh for 3600 seconds”; after that the edge revalidates (asks the origin “still good?” with an ETag/If-None-Match, getting a cheap 304 Not Modified if so). s-maxage targets shared caches (the CDN) specifically; no-store forbids caching entirely; private allows the browser but not the CDN. The TTL is the single most consequential knob: too short and your hit ratio collapses (everything revalidates, the origin gets hammered, you lose the offload); too long and you serve stale content you can’t easily take back (the hook). When you set a TTL, ask yourself: what’s the cost if this is wrong in either direction — stale data served, or origin hammered?
The number that decides whether the CDN is worth it is the cache hit ratio — hits ÷ total requests. The economics are non-linear: going from 90% to 95% hit ratio halves origin traffic (10% → 5% of requests reach origin), and 95% → 99% halves it again. Because origin compute and egress are the expensive parts, small hit-ratio gains translate into large cost and capacity wins. You raise it by setting sane TTLs, normalizing cache keys (don’t let a tracking query-string fragment the cache into a thousand near-identical copies), and origin shielding.
Origin shielding and the thundering herd
Hundreds of edge nodes mean hundreds of independent caches — and on a popular object’s first request (or right after its TTL expires), every edge can miss at once and stampede the origin with the same request: a CDN-scale thundering herd (the same failure shape as the load-balancer failover, now at the cache tier). Origin shielding fixes this by designating one (or a few) intermediate PoPs that all edge misses funnel through; the shield caches too, so the origin sees one fetch per object instead of one-per-edge. Paired with request coalescing (the edge collapses many concurrent misses for the same key into a single upstream fetch), the origin is protected even during a cache-cold spike.
▸Why this works
Why is invalidation the genuinely hard part of any cache — CDN included? Because the edge is distributed and far away: a stale copy can sit on hundreds of nodes across the planet, and you can’t reach into each one cheaply. There are two strategies. Expiry (TTL) is passive — set a short-enough TTL and staleness self-heals, but you trade hit ratio for freshness and still serve stale data up to one TTL. Purge is active — explicitly tell the CDN to evict an object (by URL) or a group (by cache tag/surrogate key), which propagates to all edges in seconds. The senior pattern that sidesteps the whole problem is cache-busting with content-hashed URLs: name immutable assets app.4f9a1c.js and serve them with a one-year TTL, then change the URL (the hash) on every deploy. The old URL is never requested again and the new one is a guaranteed miss-then-hit — you never invalidate anything, because the name itself encodes the version.
Dynamic content, edge compute, and what NOT to cache
Static assets (images, CSS, JS, video, fonts) are the CDN’s bread and butter. But CDNs increasingly handle dynamic content too: caching API responses for short TTLs, serving cached HTML with edge compute (Workers/Lambda@Edge) that personalizes at the edge, or caching the static shell and fetching only the dynamic bits. The line is moving, but the discipline is unchanged: cache aggressively what is the same for everyone and safe when slightly stale, and never cache what is per-user or must-be-correct-now.
▸Common mistake
The dangerous mistake is caching per-user or sensitive responses on a shared CDN cache. Cache an authenticated page or an API response that varies by the logged-in user, and the edge can serve User A’s bank balance, shopping cart, or session to User B who requests the same URL — a real and recurring class of data-leak incident. The rules: anything personalized or private must be Cache-Control: private (browser-only) or no-store; anything that does vary on a header (language, auth) must declare it with Vary so the cache keys on it; and never cache Set-Cookie responses on a shared cache. Also off-limits: anything that must be instantly correct (live inventory, prices you can’t tolerate being stale — the hook), and write/POST responses. When in doubt about a per-user response, the safe default is don’t cache it on the shared edge — the latency win is never worth leaking one user’s data to another.
Your CDN hit ratio is 90%; an optimization pushes it to 95%. The origin was handling 10,000 RPS of misses. Roughly what happens to origin load, and why does the small ratio change matter so much?
A pricing page is served with Cache-Control: max-age=86400 from a pull CDN. You push a price change and customers worldwide keep seeing the old price for up to a day. What happened and what's the durable fix?
To stop hundreds of edge nodes from all missing at once and stampeding the origin with the same request, a CDN funnels edge misses through one regional intermediate node that also caches — a technique called origin _______ — so the origin sees one fetch per object instead of one per edge.
- 01What two benefits does a CDN provide, and how does hit ratio drive them?
- 02Push vs pull CDN — how does each get content to the edge, and when do you use which?
- 03Why is invalidation the hard part, and what's the pattern that avoids it?
- 04What should you NOT put on a shared CDN cache, and how do you guard against leaks?
A CDN is a global fleet of edge nodes that cache content near users, buying two compounding wins: lower latency (a nearby edge instead of a far origin) and origin offload (every hit is a request the origin never sees, so 95% hits ≈ a 20× smaller origin). Content reaches the edge two ways — pull (lazy, demand-driven, the default for web traffic) and push (eager pre-warming, for large predictable assets). You control freshness with Cache-Control/TTL and revalidation, and the metric that pays for everything is the cache hit ratio, whose economics are non-linear (90%→95% halves origin traffic). Origin shielding plus request coalescing stop a CDN-scale thundering herd when popular objects go cold. The genuinely hard part is invalidation — purge is active, TTL is passive, and the pattern that avoids the problem is content-hashed URLs with long TTLs. Finally, know what not to cache on a shared edge: per-user or sensitive responses (leak risk — use private/no-store/Vary), must-be-correct data, and writes. The hook’s stale-price incident is the whole lesson: putting content at the edge is easy; taking it back is the design decision you make on purpose. Now when you see a stale-data complaint from customers after a deploy, your first questions are: what TTL did we set, did we run a purge, and are the affected URLs content-hashed?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.