Vibe Engines
YouTube
System Design

Design a CDN

Step 1 / 9

Learn system design by building a Content Delivery Network like Cloudflare, Akamai or Fastly step by step.

The numbers to beat100sPoPs worldwide~5msedge hit1st reqfills the cache

The whole design, in writing

Learn system design by building a Content Delivery Network like Cloudflare, Akamai or Fastly step by step. An interactive guide covering the origin-only baseline, edge PoP caching, anycast/GeoDNS routing to the nearest edge, cache-fill with TTL and Cache-Control, tiered shield caches that protect the origin from thundering herds, invalidation via purge and versioned URLs, edge compute, and the unhappy paths.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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.

VisitorGET /logo.png
New in this step: Visitor.

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.

What the new pieces do

Visitorclient
Someone anywhere on Earth requesting an asset — an image, script, video segment. Wants it fast, no idea where it actually comes from.

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?

VisitorOrigin
New in this step: Origin. · swipe to pan the diagram

One origin server, users everywhere. What is the core problem?

  1. Disk is cheap and easy to grow; that's not the pain. The pain is that a request from the far side of the planet is slow no matter how fast the origin is — physics, not capacity.

  2. 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.

  3. A faster CPU doesn't shorten the wire. A visitor 15,000km away still eats the speed-of-light round trip on every asset. You must serve from closer.

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.

Why this piece earns its place

Worth separating the two problems, because only one of them needs a delivery network. The single box is a capacity and availability problem, and it has a cheap local fix: a few origin replicas behind a load balancer in one region. That buys throughput and survives a dead machine, and it moves Sydney’s number by exactly nothing — the round trip is the wire, and replicas sit at the same end of it. Latency is the half that can only be paid down by moving the content. The assumption worth naming now is that this is read-heavy traffic over objects lots of people want. A cache near the user pays back when many requests want the same bytes; for a response computed fresh for one person the cache itself earns nothing, because there is no second reader to hand the copy to — whatever an edge is still worth on that traffic, it is worth for reasons that have nothing to do with storing anything. So the number I would ask for before designing any of this is what share of requests are shared, cacheable objects — that, not the request rate, decides whether everything after this is worth building.

What the new pieces do

Origindata
Your server or object store holding the real files. The CDN protects it: ideally it only ever sees cache-fill traffic.

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.

VisitorEdge PoPOrigin
New in this step: Edge PoP. · swipe to pan the diagram

How do you serve users from nearby without pre-copying everything everywhere?

  1. Full replication everywhere is wasteful (most files are never requested in most cities) and goes stale the instant content changes. You want to cache on demand, not copy everything.

  2. Users can't and won't choose a mirror, and a hardcoded mirror can't adapt when it's down or far. Routing must be automatic and content on-demand.

  3. 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.

Why this piece earns its place

Splitting traffic across hundreds of caches also splits the evidence each one has. A PoP only earns its place when the same object is asked for there again before it is evicted, so the useful question is not how many PoPs you run but how many requests per object per PoP you get. Popular objects are fine. The long tail is not: an object wanted once a week in one city misses almost every time, so the PoP carries storage and fill cost for a copy that is evicted before it is ever reused. More PoPs is therefore not monotonically better — past some point you are dicing the same traffic into thinner caches and lowering the hit ratio you were buying. The defenses are structural: give the edges a shared parent to miss into rather than letting each one fill independently, and be willing to leave big, cold objects uncached rather than evicting a hundred hot ones to store something nobody asks for twice.

  • 100sPoPs worldwide
  • ~5msedge hit
  • 1st reqfills the cache

What the new pieces do

Edge PoPcache
A cache server in a data center physically near the user — one of hundreds worldwide. A hit here returns in a few milliseconds.

Back of the envelope

pull cache, on demand
no pre-copying the whole origin
first request = miss ⇒ fetch
then served locally
popular content self-locates
near its audience

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?

VisitorGET /logo.pngAnycast / GeoDNSfind nearest PoPEdge PoPnearest cache
New in this step: Anycast / GeoDNS.

A browser resolves one hostname. How does it reach the nearest of hundreds of PoPs?

  1. Browsers don't know the PoP list and can't probe hundreds of them per request — far too slow. Nearest-routing has to happen in the network layer, transparently.

  2. 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.

  3. A single central router is exactly the far-away bottleneck a CDN exists to avoid — every request would detour through it. Routing must be distributed into the network itself.

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.)

Why this piece earns its place

Two things about anycast worth saying out loud. First, BGP’s nearest is topological, not geographic — it counts network hops and routing policy, not kilometres — so a user whose ISP peers oddly can land on a PoP much further away than the one you would have picked, and nothing in the system reports that as wrong. You find it by measuring real round-trip times per client network, which is also the standing argument for keeping GeoDNS: it is the lever for overriding the network’s answer for one region when that answer is bad. Second, be precise about what failure this heals. Anycast re-routes when a PoP stops announcing the route, so it covers a PoP that is gone. It does nothing for a PoP that is up and sick — disks full, upstream broken, returning errors quickly — because that PoP keeps advertising and keeps attracting every user for whom it is nearest. So the health check has to be wired to the announcement itself: a PoP that fails its own checks withdraws its route, and the network routes around it.

  • 1 IPannounced everywhere
  • BGPpicks nearest
  • autore-routes on failure

What the new pieces do

Anycast / GeoDNSrouting
Steers each visitor to the closest edge. Anycast announces one IP from every PoP; the network delivers to whichever is nearest.

Back of the envelope

anycast: one IP, many PoPs
network delivers to nearest
GeoDNS: resolve by location
finer control, slower to react
a dead PoP self-heals
routing skips it automatically

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?

VisitorAnycast / GeoDNSEdge PoPOrigin
The system as it stands at this step. · swipe to pan the diagram

Cached copies can go stale. How long should an edge trust its copy?

  1. One TTL for all content is wrong for most of it — a logo can cache for a year, an API response for seconds. Freshness is a property of the object, so the origin should declare it.

  2. Checking origin every time defeats caching — you're back to a round trip per request. You only want to re-check when the copy might actually be stale.

  3. 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.

Why this piece earns its place

Be clear-eyed about what a revalidation costs. A 304 is cheap in bytes and not at all cheap in time — it is still a full round trip from the edge to the origin, with a user sitting on it. So a short max-age does not buy freshness for free; it quietly converts a share of your requests back into origin-latency requests while the object still counts as cached. A page can look like it is being served from the edge and still carry the origin’s tail latency if the TTLs are set in seconds. That is why the answer to wanting fresher content is almost never a smaller number here. And the direction of the mistake matters, because a long TTL is not reversible: the header goes to every cache in the path, including the visitor’s browser, and you cannot purge a browser. Whatever you mark cacheable for a year, you are committed to for a year, on machines you will never touch — which is why long, confident TTLs belong on assets whose URL you control, not on the document that names them.

  • max-ageorigin sets TTL
  • 304cheap revalidate
  • immutablenever re-check

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?

Edge PoPShield CacheOrigin
New in this step: Shield Cache. · swipe to pan the diagram

On a fresh object, hundreds of edges miss at once and all hit origin. How do you protect it?

  1. Scaling the origin fights the symptom, not the cause — you still take N simultaneous fetches for one object every time something new goes viral. Collapse the fan-in instead.

  2. 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.

  3. Backoff spreads the stampede out in time but the origin still eats every distinct fetch, just slower. You want to collapse many misses into one, which is what a shield tier does.

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.

Why this piece earns its place

Both halves of this have a cost worth naming. The shield is another hop, so a miss now pays edge to shield to origin; that is a win only if the parent is genuinely regional, because a shield on another continent turns origin protection into a latency tax on every miss. And coalescing changes the shape of failure. Independent requests fail independently; a coalesced one has shared fate. Ten thousand waiters now hang on a single upstream fetch, so a stalled origin connection does not fail one request, it freezes all of them at once — and when they give up together the retry burst is worse than the stampede you were preventing. So the leader’s fetch needs a hard timeout, and the waiters need somewhere defined to go when it expires: a stale copy, a capped queue, an explicit error. Collapsing the fan-in is right. Just decide deliberately what happens when the one request everything depends on does not come back.

  • 1000s : 1edges : origin fetch
  • coalescedmisses share one fetch
  • 2 tiersedge → shield → origin

What the new pieces do

Shield Cachecache
A regional parent cache sitting between the edges and the origin. Many edges miss into one shield, so the origin sees one fetch, not thousands.

Back of the envelope

edge → shield → origin
each tier collapses fan-in
request coalescing
concurrent misses = one fetch
origin sees ~1 req / object / region
not thousands

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?

Anycast / GeoDNSfind nearest PoPEdge PoPtier-1 cachePurge / ControlinvalidationShield Cacheregional parent
New in this step: Purge / Control.

A bad file is cached for a year across 300 PoPs. How do you replace it now?

  1. Tiny TTLs mean constant revalidation — you trade away almost all the cache benefit to buy freshness you rarely need. Cache long, and invalidate precisely when you actually change something.

  2. A year is not "now". Natural expiry is fine for routine freshness but useless for an urgent fix. You need an explicit lever.

  3. 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.

Why this piece earns its place

Versioning does not remove invalidation, it relocates it. Something has to name app.9f3a1.js, and that something is the HTML document, which by definition cannot be fingerprinted — so all the pressure you just took off hundreds of assets now sits on one entry document that has to be short-TTL and purgeable. It also means a deploy is never atomic across the edges: for as long as the old document is cached anywhere, some visitors are running old HTML and will ask for the old asset. The rule that travels with versioning is therefore that you never purge the previous versions — keep them and let them age out — or a deploy hands a slice of your users a broken page that reloading does not fix. On the purge side, treat the fan-out as a distributed system rather than a broadcast. A PoP that was rebooting or partitioned when the command went out comes back holding the old object and serves it happily, so what it reconciles against on startup should be a durable version it can compare, not a message it had to be awake to hear.

  • content hashnew file = new URL
  • purgeevict in seconds
  • soft purgestale-while-revalidate

What the new pieces do

Purge / Controlcontrol
The control plane. Pushes purge commands and new config to every edge so stale content can be evicted on demand.

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.

Edge PoPShield CacheEdge ComputeOriginEdge Logs
New in this step: Edge Compute, Edge Logs. · swipe to pan the diagram

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.

Why this piece earns its place

The real cost of computing at the edge is what it does to the cache. A response that varies per visitor is by definition not shareable, so the moment a function personalizes a body you have to tell the cache what the answer depends on — and getting that wrong in the permissive direction caches one visitor’s page and hands it to everyone behind that PoP, which is the worst bug on this page precisely because it is fast, quiet and correct-looking. Getting it wrong in the strict direction is merely expensive: every dimension you add to the key multiplies the copies each PoP must store and fill, so keying on something high-cardinality like a user id makes every request a miss and you have built an elaborate proxy. Keep the varying dimension small and enumerable — a country, a device class, an experiment bucket — and push genuinely per-user work into the things that do not change the cached body: headers, redirects, an auth decision taken before the lookup.

What the new pieces do

Edge Computeservice
Code that runs at the edge — auth, A/B, header rewrites, personalization — so dynamic responses stay near the user too.
Edge Logsdata
Every edge streams request logs asynchronously for analytics, billing and security — never on the hot path.

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.

VisitorAnycast / GeoDNSEdge PoPPurge / ControlShield CacheEdge ComputeOriginEdge Logs
The system as it stands at this step. · swipe to pan the diagram

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.

Why this piece earns its place

Stale-if-error is a real shield with a shape worth stating: it can only serve what a PoP already holds, so its coverage is whatever was popular enough to be cached there, and that coverage decays. The longer an outage runs, the more entries fall out of even the stale window, so availability degrades over hours rather than holding flat — and how long you are willing to serve stale is a per-content decision, since serving a price or an offer long after the origin went dark is its own kind of incident. But the failure I would actually plan for is not a dead origin. A dead origin is the easy case: it errors, and these rules fire. The dangerous one is an origin that is healthy and wrong — a bad deploy answering with an empty body, a misconfigured bucket serving a default page. The edge cannot tell, so it caches that as the truth, spreads it everywhere and serves it fast. Which is why a cache fill deserves its own sanity checks on status, content type and plausible size, and why the purge path from the previous step is a lever you must be able to pull in a hurry.

You did it

You just designed a CDN.

VisitorAnycast / GeoDNSEdge PoPPurge / ControlShield CacheEdge ComputeOriginEdge Logs
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Distance is the enemy — serve copies from PoPs near the user, not from one far origin.
  • Edges are pull caches: first request fills, the rest are local hits.
  • Anycast (one IP, nearest PoP) + GeoDNS route users to the closest edge and self-heal on failure.
  • The origin declares freshness with Cache-Control/max-age + ETag; edges revalidate cheaply with 304.
  • Tiered shield caches + request coalescing turn thousands of edge misses into one origin fetch.
  • Versioned URLs sidestep invalidation; explicit purge is the escape hatch for fixed URLs.
  • Edge compute + async logs; stale-while-revalidate and stale-if-error keep it fast and up.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Anycast vs GeoDNS — when does each win?

    Anycast advertises one IP from every PoP and lets BGP deliver to the topologically nearest — it re-routes automatically in seconds when a PoP fails, which makes it great for availability and DDoS absorption, but you have less explicit control over exactly which PoP a user hits. GeoDNS resolves the hostname to a specific PoP IP based on the resolver's location, giving finer geographic control and easier per-region policy, but it reacts slower because DNS answers are cached by resolvers. Large CDNs layer both: GeoDNS for coarse steering, anycast for fast failover.

  2. Why is cache invalidation considered so hard?

    Because "make every one of hundreds of independent caches forget X at the same instant" is a distributed-consistency problem with no free answer. The elegant escape is to avoid it: fingerprint content into the URL (app.9f3a1.js) so a change is a brand-new key that was never cached — the old key just ages out harmlessly and you can cache everything for a year. When the URL must stay fixed (an HTML document, an API path), you fall back to an explicit purge fan-out from the control plane, ideally a soft purge that serves stale-while-revalidating so the purge itself doesn't stampede the origin.

  3. How does a CDN keep working when the origin is down?

    Any object already cached and unexpired keeps serving from the edge with zero origin involvement — that alone covers most traffic. For expired objects, stale-if-error lets the edge serve the last known copy rather than an error when the origin refetch fails, trading a little staleness for availability. Only truly-uncached objects during an origin outage will fail. This is why a CDN is an availability and DDoS shield, not just a latency optimization: it decouples serving from the origin being healthy.

  4. What is request coalescing / collapsed forwarding and why does it matter?

    When many clients request the same uncached object simultaneously, a naive cache issues one upstream fetch per client — a stampede that can crush the origin exactly when an object goes viral. Coalescing detects concurrent misses for the same key, issues a single upstream fetch, and lets all the waiters share its result. Combined with a tiered shield (many edges collapse into a regional parent, which itself coalesces), the origin sees roughly one fetch per object per region no matter how large the crowd.

  5. Static vs dynamic content — what can a CDN actually accelerate?

    Static, cacheable assets (images, JS/CSS, video segments, fonts) are the classic win — cache once, serve everywhere. Dynamic or personalized responses can't be cached blindly, but a CDN still helps three ways: it terminates TLS and keeps a warm connection to origin so users still get a nearby handshake; edge compute runs auth/personalization/routing at the PoP instead of the origin; and micro-caching (a 1–5s TTL on "dynamic" pages) can absorb huge read bursts on content that changes less often than it's requested.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A CDN makes assets fast primarily by…

    Latency is bounded by distance; moving copies close to users removes the cross-planet round trip.

  2. Users reach the nearest PoP because…

    Anycast (and GeoDNS) route at the network/resolution layer and self-heal when a PoP dies.

  3. How long an edge trusts its cached copy is decided by…

    The origin declares freshness per object; the edge revalidates cheaply with a 304 on expiry.

  4. A tiered shield cache exists to…

    Many edges miss into one regional shield, which coalesces concurrent misses into a single origin fetch.

  5. The cleanest way to change a long-cached asset immediately is…

    Fingerprinted URLs sidestep invalidation entirely; explicit purge is the fallback for fixed URLs.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Serve nearby: deliver each asset from a PoP physically near the visitor, not the far origin.
  • Route to nearest: land every user on the closest PoP behind one hostname.
  • Stay fresh: honor the origin’s Cache-Control TTL and revalidate cheaply on expiry.
  • Protect the origin: collapse many edge misses into roughly one origin fetch.
  • Invalidate: replace a long-cached asset now via versioned URLs or an explicit purge.

The qualities that shape everything

Each one names the mechanism that buys it.

Low latency for users worldwide
PoPs hold copies near the user as pull caches — the first request fills, every later one is a local hit in a few milliseconds.
Reach the nearest PoP and self-heal
Anycast advertises one IP from every PoP so the network delivers to the topologically nearest and re-routes automatically when one dies (GeoDNS as the finer-grained alternative).
Fresh without re-fetching every time
The origin declares a per-object TTL via Cache-Control; the edge serves until expiry, then does a cheap conditional revalidation (304 Not Modified) via ETag.
Origin survives a thundering herd
A regional shield (parent) cache with request coalescing folds thousands of concurrent edge misses into a single origin fetch per object.
Replace long-cached content immediately
Versioned/fingerprinted URLs make a change a brand-new key that was never cached, with an explicit control-plane purge as the escape hatch for fixed URLs.
Stay fast and up under failure
Stale-while-revalidate serves instantly through an expiry, stale-if-error serves through an origin outage, and coalescing keeps one refresh serving the whole stampede.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Serving from closer (edges) over a bigger, faster origin

A faster CPU or more disk can’t shorten the wire — a visitor 15,000km away still eats the speed-of-light round trip on every asset. You reduce distance, not scale one box.

On-demand pull caches over pre-replicating the origin everywhere

Full replication wastes space on files no one in a region requests and goes stale the instant content changes. A pull cache fills only what’s actually asked for, near its audience.

Anycast routing over a central router

A single central router is exactly the far-away bottleneck a CDN exists to avoid — every request would detour through it. One IP announced from every PoP lets the network deliver to the nearest and skip a dead PoP automatically.

A tiered shield with coalescing over adding more origin servers

Scaling the origin fights the symptom — you still take N simultaneous fetches for one fresh object every time something goes viral. A regional shield collapses many edge misses into a single coalesced origin fetch.

Versioned URLs (with purge) over lowering every TTL to seconds

Tiny TTLs trade away almost all the cache benefit for freshness you rarely need. Fingerprinting content into the URL makes a change a new key that was never cached; purge is the escape hatch when the URL must stay fixed.

The answer, out loud

What a strong answer to “Design a CDN” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Scope it before drawing

    Let me scope this before I draw anything. We are delivering assets — images, scripts, video segments — to people all over the world, and what I am buying back is time on the wire. So the first number I want on the board is how much of the traffic is the same bytes for lots of people, because that share is the part a delivery network can help with at all. I would also ask where the audience actually sits and how often the content changes.

  2. 3–8 min

    Start from one origin

    I will start from one origin serving everyone, because it makes the problem visible. It is correct, and it has two problems that usually get confused with each other. Everything funnels through one machine, and a visitor on the far side of the planet pays a round trip on every single asset. Sydney to Virginia is about a hundred and sixty milliseconds at best, and that is light in fibre, so there is no faster server that fixes it. I want to split those two early, because the first has ordinary answers and the second has only one: be closer.

    Built in step 1: One origin, the whole world
  3. 8–15 min

    Copies near people

    So I put caches near the users, a few hundred of them. I do not want to pre-copy the origin into all of them, because most files are never asked for in most cities and a copy goes stale the moment the content changes. I would make them pull caches instead. The first request in a region misses, fetches from the origin and keeps the copy; everything after that is served locally in a few milliseconds. The property I like is that content ends up near the people who want it without anyone deciding that, and cold objects never take up space.

    Built in step 2: A cache near every user
  4. 15–21 min

    Landing on the nearest one

    Then the routing question, which is the one people skip: the browser resolves one hostname, so how does it reach the closest of hundreds of caches? I would not put a router in the middle — that is the far-away box I just spent ten minutes removing. I would use anycast: every PoP announces the same address and the internet’s own routing hands each user to the nearest one. A dead PoP stops announcing and the traffic moves without anyone being paged. I would keep GeoDNS alongside it for the cases where I want to steer a region explicitly, since it is slower to react but more precise.

    Built in step 3: How does the user reach the closest PoP?
  5. 21–27 min

    How long a copy is good for

    Freshness next. I do not want the edge guessing, and I do not want it asking the origin on every request, because that is the round trip I just removed. The origin should declare it per object with Cache-Control — how long this thing is good for — and the edge serves freely until then. On expiry it asks a cheap question using an ETag, still valid, and the origin can answer three-oh-four with no body at all. The origin owns the policy, the edge enforces it, and a logo and a price can have completely different answers.

    Built in step 4: When can the edge trust its copy?
  6. 27–33 min

    Keeping the origin out of the path

    Now the thing that bites in production. Something new goes viral, every PoP misses on it in the same instant, and the origin gets stampeded by its own delivery network. Adding origin servers does not fix that, it just spreads it around. What I would add is a tier: edges do not miss to the origin, they miss into a regional parent, and that parent folds concurrent misses for the same object into one upstream fetch while the rest wait and share the result. Then the origin sees roughly one request per object per region however big the crowd gets.

    Built in step 5: A thousand edges, one origin
  7. 33–39 min

    Changing something already everywhere

    Invalidation is the part I would want to get right in the room, because it is where the design is actually opinionated. The instinct is to keep every TTL short so changes propagate quickly, and I would argue against it: you pay for freshness constantly to buy something you need rarely. Instead I would put the content hash in the filename. A change is then a brand-new URL that was never cached, so nothing can be stale, and the old one can cache effectively forever. Purge is the escape hatch for the URLs I cannot change, like an HTML page, and I would prefer a soft purge that keeps serving the old copy while the new one is fetched.

    Built in step 6: Invalidation: the hard problem
  8. 39–43 min

    The parts that are not files

    Two more pieces before I close. Not everything is a static file, so I would run small functions at the PoP for auth, redirects and experiments, which keeps dynamic responses near the user too. And every edge streams its request logs out of band for analytics, billing and attack detection — useful, and never on the path of the response.

    Built in step 7: Compute and see at the edge
  9. 43–45 min

    Close on the trade-off

    The trade-off across all of it is freshness against distance: everything I did to make this fast put the truth further away, and every mechanism here is a different way of bounding how wrong a copy is allowed to be. With more time I would work the unhappy paths properly — serving stale while refreshing behind it so an expiry never blocks a user, serving stale when the origin is down so the cache doubles as an availability shield — and I would look at micro-caching the dynamic pages for a second or two, which absorbs an enormous read burst on content that changes less often than it is asked for.

What this teaches

Learn system design by building a Content Delivery Network like Cloudflare, Akamai or Fastly step by step. An interactive guide covering the origin-only baseline, edge PoP caching, anycast/GeoDNS routing to the nearest edge, cache-fill with TTL and Cache-Control, tiered shield caches that protect the origin from thundering herds, invalidation via purge and versioned URLs, edge compute, and the unhappy paths.

Key takeaways

  • Distance is the enemy — serve copies from PoPs near the user, not from one far origin.
  • Edges are pull caches: first request fills, the rest are local hits.
  • Anycast (one IP, nearest PoP) + GeoDNS route users to the closest edge and self-heal on failure.
  • The origin declares freshness with Cache-Control/max-age + ETag; edges revalidate cheaply with 304.
  • Tiered shield caches + request coalescing turn thousands of edge misses into one origin fetch.
  • Versioned URLs sidestep invalidation; explicit purge is the escape hatch for fixed URLs.
  • Edge compute + async logs; stale-while-revalidate and stale-if-error keep it fast and up.

Concepts covered

  • What is a CDN?
  • One origin, the whole world
  • A cache near every user
  • How does the user reach the closest PoP?
  • When can the edge trust its copy?
  • A thousand edges, one origin
  • Invalidation: the hard problem
  • Compute and see at the edge
  • Stale, stampede & origin-down
built to be everywhere at once — make the calls, drop the edge, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs