Design a CDN — the walkthrough in full
A written version of the interactive walkthrough above — the same steps, decisions and trade-offs, laid out for reading, reference and search.
The big idea
What is a CDN?
A visitor in Sydney requests an image hosted on a server in Virginia. Light itself takes ~80ms each way; add the round-trips of a TCP+TLS handshake and the file is slow before a single byte of it moves. Do that for every image, script and video on the page, for every user on Earth.
A Content Delivery Network fixes distance the only way you can: don't move the user closer to the server — move copies of the content closer to the user. Hundreds of cache servers worldwide each hold a copy, and every visitor is served from the one nearest them.
How to read this: Each step opens with a real design decision — make the call before I show you what ships. Watch the diagram grow, hover the boxes, and at the end kill a PoP and kill the origin to see what a CDN really buys you. Hit Begin.
Step 1 · The baseline
One origin, the whole world
Start with the naive setup: one origin server holds every file, and every visitor on Earth talks to it directly. It's correct — everyone gets the right bytes. So what's actually wrong with it?
Design decision: One origin server, users everywhere. What is the core problem?
The call: Distant users pay unavoidable round-trip latency, and every request hits one box. — Distance is fixed latency you can't optimize away at the origin, and concentrating all traffic on one origin makes it both slow and a single point of failure.
Two problems, one root cause: distance. Every request pays the full round-trip to a far-away origin (latency you can't code away), and funnelling the planet through one machine makes it a bottleneck and a single point of failure. The fix isn't a bigger origin — it's serving from closer.
Latency is distance: Round-trip time is bounded by the speed of light in fiber (~200,000 km/s) plus every hop's queuing. Sydney↔Virginia is ~160ms round trip at best. You can't out-engineer geography from one location — you have to reduce the distance.
Step 2 · Move content to the edge
A cache near every user
If distance is the enemy, put a copy of the content near the user. But a copy where, and holding what — you can't pre-load every file to every city, and content changes.
Design decision: How do you serve users from nearby without pre-copying everything everywhere?
The call: Put a cache (an edge PoP) near users; it fetches from origin on the first miss, then serves copies. — The edge lazily pulls each object from origin on its first request and holds it. Later requests for that object in that region are served locally — a few ms instead of a cross-planet round trip.
Deploy Points of Presence (PoPs) — cache servers in data centers around the world. A PoP holds nothing until asked; on the first request for an object it fetches from origin (a miss) and stores the copy. Every later request in that region is a hit, served locally in milliseconds.
Pull, don't push: Edges are pull caches: content flows to a PoP only when someone there actually asks for it. Popular objects end up cached near their audience automatically; cold objects never waste space. The origin is touched once per object per PoP, then rarely again.
Step 3 · Find the nearest edge
How does the user reach the closest PoP?
Hundreds of PoPs exist — but the visitor's browser just resolves cdn.example.com and opens a connection. How does that connection land on the nearest PoP and not a random one across the world?
Design decision: A browser resolves one hostname. How does it reach the nearest of hundreds of PoPs?
The call: Anycast: every PoP announces the same IP, and the network delivers to the nearest one. — With anycast the same IP is advertised (via BGP) from every PoP; internet routing naturally hands each user's packets to the topologically closest one. GeoDNS is the alternative — return a PoP IP based on the resolver's location.
Use anycast: every PoP advertises the same IP address via BGP, and the internet's own routing delivers each user's packets to the topologically nearest PoP — no central decision, no extra hop. (GeoDNS is the classic alternative: resolve the hostname to a different PoP IP based on where the query came from.)
Anycast vs GeoDNS: Anycast routes at the network layer — one IP, nearest wins, and it re-routes automatically when a PoP dies. GeoDNS routes at resolution time by geography, giving finer control but reacting slower (DNS is cached). Big CDNs use both.
Step 4 · Freshness
When can the edge trust its copy?
A cached copy is fast, but content changes — a new logo, an updated price. If the edge serves its copy forever, users see stale bytes. If it re-checks origin every time, you've thrown away the whole benefit. Who decides how long a copy is good for?
Design decision: Cached copies can go stale. How long should an edge trust its copy?
The call: The origin declares a TTL via Cache-Control headers; the edge serves until it expires, then revalidates. — Cache-Control: max-age and ETag/Last-Modified let the origin say exactly how long each object is fresh. The edge serves freely until expiry, then does a cheap conditional revalidate (304 Not Modified) instead of a full refetch.
The origin decides, per object, with HTTP Cache-Control headers. max-age sets the TTL; the edge serves the copy freely until it expires. On expiry it does a cheap conditional revalidation — "still valid?" via ETag/If-None-Match — and the origin answers 304 Not Modified (no body) if nothing changed.
TTL + ETag: Cache-Control: max-age=31536000, immutable on a versioned asset means "cache for a year, never re-check". Short-lived content uses a small max-age plus an ETag so revalidation is a tiny 304 instead of resending the whole object. Freshness is declared by the origin, enforced at the edge.
Step 5 · Protect the origin
A thousand edges, one origin
A brand-new viral video: hundreds of PoPs all miss on it at the same instant and every one stampedes the origin for the same file. The origin — the one thing you were protecting — gets hammered by its own CDN. How do you absorb the fan-in?
Design decision: On a fresh object, hundreds of edges miss at once and all hit origin. How do you protect it?
The call: Add a tiered shield: edges miss into a regional parent cache that fetches origin once. — A shield (parent) cache sits between edges and origin. Many edges miss into one shield; the shield does a single origin fetch (with request coalescing), so the origin sees one request per object per region, not thousands.
Add a tiered cache. Edges don't miss straight to origin — they miss into a regional shield (parent) cache. Many edges fold into one shield, and the shield uses request coalescing (a.k.a. collapsed forwarding) so concurrent misses for the same object become a single origin fetch. The origin sees one request per object per region.
Tiered caching: Two cache layers: many edge PoPs → a few regional shields → origin. Each layer collapses the fan-in of the one below. Coalescing means that even within a shield, 10,000 simultaneous misses for one URL trigger exactly one upstream fetch; the rest wait and share the result.
Step 6 · Change it now
Invalidation: the hard problem
You shipped a bug in app.js and it's cached at a year-long TTL across 300 PoPs. You need every edge to stop serving the old copy now, not in a year. But you can't reach out and edit 300 caches instantly and cheaply. How?
Design decision: A bad file is cached for a year across 300 PoPs. How do you replace it now?
The call: Version the URL (content hash) and/or issue a purge to all edges via the control plane. — Two tools: (1) versioned/fingerprinted URLs (app.9f3a1.js) make a change a brand-new URL that was never cached, so it can't serve stale — and you cache the old one forever harmlessly. (2) An explicit purge command from the control plane evicts a key from every PoP in seconds for cases where the URL is fixed.
Two complementary levers. Versioned URLs: fingerprint assets by content hash (app.9f3a1.js) so any change is a new URL — never stale, and the old one can cache forever. For fixed URLs (like an HTML page), issue an explicit purge from the control plane that evicts the key from every PoP in seconds. Prefer versioning; purge when you can't change the URL.
Cache invalidation: "There are only two hard things in CS…" — versioning sidesteps invalidation entirely (new content ⇒ new key). Purge is the escape hatch: a fan-out from a central control plane to all edges. soft purge marks stale-but-servable (serve old while refetching) to avoid a stampede the instant you purge.
Step 7 · Beyond static files
Compute and see at the edge
Not everything is a static image. You want auth checks, A/B tests, redirects, personalization, and — separately — you need to know what every PoP is doing for analytics, billing and attack detection. Doing that at the origin re-introduces the round trip you just removed.
Run edge compute: small functions that execute at the PoP (rewrite headers, do auth, personalize, route) so even dynamic responses stay near the user. And stream request logs from every edge asynchronously to a central pipeline — used for analytics, billing and DDoS/WAF detection — never blocking the response.
The edge is a platform: Once you have compute in hundreds of locations, a CDN becomes more than a cache: TLS termination, a Web Application Firewall, DDoS absorption (soak an attack across global capacity), and programmable routing all live at the edge. Logs flow out of band so the hot path stays hot.
Step 8 · The sharp edges
Stale, stampede & origin-down
Real traffic finds the corners: an object expires under sudden load (a cache stampede), the origin goes down mid-refresh, or a region needs content it's never seen.
Use stale-while-revalidate — serve the slightly-stale copy instantly while refreshing in the background — so an expiry never blocks a user. Combine it with coalescing so one refresh serves the whole stampede. Serve stale-if-error when the origin is down, turning the cache into an availability shield. And keep TTLs long with versioned URLs so freshness costs you nothing.
Design for the unhappy path: Expiry under load → stale-while-revalidate. Origin down → stale-if-error. Simultaneous misses → coalescing. A cache that only works on a hit is a demo; a cache that degrades gracefully on miss, expiry and origin failure is a CDN.
You did it
You just designed a CDN.
- D — i
- E — d
- A — n
- T — h
- T — i
- V — e
- E — d