← System design

Infrastructure

Design CDN

A content delivery network puts copies of other people's files close to the people who want them. The idea is simple; the engineering is in the details: steering each user to a nearby site without trusting a slow DNS lookup, keeping hit ratios high across 180 locations without storing every file 180 times, protecting origins from stampedes, and making a purge reach every server on Earth in a couple of seconds. Think Cloudflare, Akamai, Fastly or CloudFront.
Core: anycast edges + parent tier + tag purge30M requests a second at peak180 PoPs, about 14 Tbps out95% of requests served at the edge

01

Requirements

02

Requirements

Functional
  • Customers point a hostname at the CDN and we serve their static assets, images, downloads and video segments from cache.
  • Customers set caching rules per path: TTLs, which query parameters and headers form the cache key, and whether cookies bypass the cache.
  • Customers purge content by URL, by prefix or by tag, and see it gone everywhere within seconds.
  • TLS for every customer hostname, with automatic certificate issuance and renewal.
  • Built-in protection: rate limits, a web application firewall and absorption of volumetric attacks.
  • Near real-time logs and dashboards: requests, bytes, hit ratio, errors and latency per hostname.
Non-Functional
  • Most users reach a site within 30 ms round trip in well-connected regions.
  • Cache hits answered in under 10 ms of server time; first byte at p95 under 100 ms for hits worldwide.
  • Edge request hit ratio of 95% or better for typical static workloads.
  • A full site going dark costs users a reconnect, not an outage; the network as a whole targets 99.99% or better.
  • A purge reaches every site in under 2 seconds at p99.
  • Origins see only a small fraction of traffic, even right after a purge.
Ask about the workload mix

Static assets, video and API acceleration behave very differently. Ask what share of bytes is video, how big the largest objects are, and whether dynamic content is in scope. Video pushes you to chunked caching and large disks; APIs push you to connection reuse and edge compute instead of caching.

02

Sizing the network

03

Sizing the network

Assume a CDN with 60,000 customer zones, 180 points of presence (PoPs) and a mixed workload: many small web assets plus video segments that dominate the bytes. Average response size across all requests is about 60 KB.

QuantityWorkingResult
Peak requestsevening peak across the busiest regionsabout 30M / s
Peak egress30M / s x 60 KBabout 1.8 TB / s, roughly 14 Tbps
Busiest PoP shareabout 4% of global traffic1.2M req / s, about 580 Gbps
Edge servers in that PoP580 Gbps / 25 Gbps usable each, plus N+2about 26 servers
Cache per server512 GB RAM for hot objects, 60 TB NVMeabout 1.5 PB disk per large PoP
Requests missing the edge5% of 30M / sabout 1.5M / s to the parent tier
Requests reaching originsparent tier absorbs about 70% of edge missesabout 450K / s across all customers
Log lines30M / s x about 400 Babout 12 GB / s before sampling and compression
Purge requests60K zones, bursty deploysabout 5K / s at peak, each fanned to 180 PoPs
TLS certificatesone or more per customer hostnameabout 2M managed certificates
The shape of the problem

Egress dominates cost, and every point of hit ratio is worth real money: going from 95% to 96% removes a fifth of the remaining origin and backbone traffic. That is why most of the design is about getting more hits out of the same disks.

Long tail

A popular PoP sees millions of distinct objects a day, and most are requested only once or twice. The cache must stop those one-hit objects from evicting the ones that will be requested again.

03

API design

04

API design

Customers interact with two surfaces: a control API for configuration and purges, and HTTP headers on their origin responses that tell the edge how to cache.

PUT/v1/zones/{zone_id}/rulesReplace cache and routing rules
{ "version": 412, // optimistic concurrency "origin": { "host": "origin.shop.example", "shield": "fra" }, "rules": [ { "match": "/static/*", "edge_ttl": "30d", "key_query": [] }, { "match": "/img/*", "edge_ttl": "7d", "key_query": ["w", "fmt"] }, { "match": "/account/*", "bypass": true } ] } // 202: live on every PoP once the config version shows "deployed"
POST/v1/zones/{zone_id}/purgeInvalidate content
{ "tags": ["product-8812", "category-shoes"], "mode": "soft" } // soft: mark stale and revalidate on next request; hard: delete outright { "purge_id": "pg_7c01", "status": "propagating" }
GET/v1/purges/{purge_id}Track propagation
{ "status": "complete", "pops_acked": 180, "pops_total": 180, "elapsed_ms": 1340 }
GETorigin response headersHow the origin steers caching
Cache-Control: public, max-age=60, s-maxage=86400, stale-while-revalidate=300, stale-if-error=86400 Surrogate-Key: product-8812 category-shoes Vary: Accept-Encoding // edge adds on the way out: X-Cache: HIT, edge=bom2, parent=MISS
GET/v1/zones/{zone_id}/analytics?since=-1h&by=popTraffic and hit ratio
{ "requests": 48201193, "hit_ratio": 0.962, "origin_requests": 611204, "p95_ttfb_ms": 41 }

04

Architecture

05

Architecture

Data plane from user to origin, plus the control planeDiagram
flowchart LR
	U["Users"] --> AC["Anycast IP, BGP picks the nearest PoP"]
	AC --> LB["L4 balancer in the PoP"]
	LB --> ES["Edge server: TLS, WAF, routing"]
	ES --> CH["Owner of the cache key in this PoP"]
	CH -->|miss| PT["Parent tier in a regional hub"]
	PT -->|miss| OR["Customer origin"]
	CP["Control plane"] --> CFG[("Config and cert store")]
	CFG -. "push" .-> ES
	CP --> PB["Purge bus"]
	PB -. "fan out" .-> CH
	PB -. "fan out" .-> PT
	ES -. "logs" .-> LP["Log pipeline and analytics"]

Every customer hostname resolves to a small set of anycast addresses that all 180 PoPs announce through BGP. The internet's routing picks the path to the "closest" PoP in network terms, which is usually close in distance too. Inside the PoP, an L4 load balancer spreads connections across edge servers with consistent hashing on the connection tuple, so packets of one TCP or QUIC flow keep landing on the same server.

The edge server terminates TLS, applies the firewall and rate limits, matches the request against the zone's rules and builds the cache key. It then hashes that key to find which server in the PoP owns the object. Sharding cache by key inside the PoP means 26 servers act as one large cache instead of 26 copies of the same hot set. If the owner has the object, it streams it back through the edge server that took the request.

On a miss, the owner asks a parent tier: a handful of large regional hubs, each acting as shield for many edge PoPs. Only a parent miss reaches the customer's origin. The control plane is separate: it stores configurations and certificates, pushes versioned changes to every server, fans out purges, and collects logs. The data plane keeps serving with its last known config if the control plane is unreachable.

Anycast front door

One IP everywhere. A site that fails withdraws its routes and traffic shifts to the next nearest PoP in seconds, without waiting for DNS TTLs.

Sharded PoP cache

Consistent hashing of the cache key across servers in the PoP. A very hot object is copied to every server's memory so one machine is not the bottleneck.

Parent tier

Regional shields turn many edge misses into one origin fetch. They have bigger disks and keep long-tail objects that individual PoPs evict.

Control plane

Config, certificates and purges flow out; logs and metrics flow in. It is built so that its own outage never stops content delivery.

05

Deep dive: routing, the cache hierarchy and purge

06

Deep dive: routing, the cache hierarchy and purge

Where the design is decided

Three things separate a real CDN from a reverse proxy in many cities: how users find the right site, how sites share work so hit ratio stays high, and how a change at the origin becomes visible everywhere fast.

One request through the tiersDiagram
flowchart TD
	R["Request arrives at edge"] --> K["Build cache key from host, path, chosen query and headers"]
	K --> O["Hash key to owner server in PoP"]
	O --> M{"Fresh copy on owner?"}
	M -->|yes| H["Serve hit"]
	M -->|stale| SWR["Serve stale, refresh in background"]
	M -->|no| C{"Fetch already in flight?"}
	C -->|yes| W["Wait on the same fetch"]
	C -->|no| P["Ask parent tier"]
	P --> PO{"Parent has it?"}
	PO -->|yes| F["Store and serve"]
	PO -->|no| OR["Fetch from origin, store at both tiers"]

Getting users to the right site

There are two ways to steer users. DNS steering answers each lookup with the IP of a PoP chosen for that resolver. It allows fine control over load, but it sees the resolver, not the user, and resolvers can be far from their users. A change also takes as long as the DNS TTL plus whatever caching resolvers do on top.

Anycast announces the same prefix from every PoP and lets BGP choose. Failover is fast: when a PoP stops announcing, routers converge on the next path in seconds. It also spreads attack traffic naturally, because a botnet spread around the world hits many sites at once. The cost is less control. BGP does not know about load, and a routing change can move a live TCP connection to a different PoP, which then has no state for it. In practice, routes are stable enough that this is rare, and QUIC connection ids help.

Most large CDNs mix both. They use anycast for the front door, then tune it with BGP communities and prefix splits when one site is overloaded, and sometimes use DNS to steer very large customers or video traffic with precision.

Making the cache hit

Cache key design comes first. By default the key is scheme, host, path and the full query string, plus whatever the response lists in Vary. Most real gains come from removing noise: drop tracking parameters such as utm_source, sort the remaining ones, and normalise Accept-Encoding into a few buckets. A single unnormalised parameter can split one object into thousands of copies.

Inside a server, two layers work together: RAM for small, very hot objects and NVMe for everything else. Pure LRU lets a crawl of one-hit objects push out the useful set, so the admission policy matters as much as eviction. A common approach is to admit an object to disk only on its second request within a window, tracked in a small counting filter, and to evict with an LRU variant that protects frequently reused objects.

Large files need a different shape. A 4 GB game patch or an hour of video should not be one cache entry. Store objects in fixed slices of about 2 MB, keyed by object and slice number, and serve range requests by assembling slices. A user who watches the first two minutes then leaves only pulls the slices they watched, and different slices of one file can live on different servers.

Protecting the origin

Three mechanisms keep origins safe. The parent tier turns misses from many PoPs into a single fetch per region. Request coalescing makes sure that if 3,000 requests for the same uncached object arrive at once, only one goes upstream and the rest wait on it. And stale serving lets the edge return an expired copy while it revalidates in the background (stale-while-revalidate), or for hours if the origin is failing (stale-if-error).

Revalidation is cheap when origins send ETag or Last-Modified: the parent asks "has this changed?" and gets a 304 with no body if not. That keeps expired-but-unchanged content flowing at the cost of one small request.

Purge in two seconds

A purge request lands in the control plane, which assigns it a global sequence number and writes it to a durable log. A fan-out service publishes it to every PoP over persistent connections, and inside the PoP a local relay delivers it to every server. Each server acknowledges, and the control plane reports progress per PoP. Two seconds is achievable because purges are tiny messages pushed over warm connections, not pulls.

Purging by URL is easy; purging by tag is what customers actually need. When an origin response carries Surrogate-Key: product-8812, the server records that key in a local index from tag to cache entries. Purging the tag then removes every page, image and API response that mentioned the product. A soft purge only marks entries stale, so the next request revalidates instead of every PoP hitting the origin at once.

Servers that were offline during a purge must catch up before they serve. Each server remembers the last purge sequence it applied, and on reconnect it replays the missing range from the log. If the gap is too old to replay, it drops the affected zones' cache entirely.

A purge is a promise

If a customer deletes a leaked document and purges it, one forgotten server still serving it is an incident. That is why purges are acknowledged per server and why a lagging server is pulled out of rotation until it catches up.

06

Tradeoffs and design choices

07

Tradeoffs and design choices

Chosen
Anycast front door with BGP tuning

Fast failover, attack traffic spread across sites, and no dependency on resolver location. You accept coarser load control and the rare connection that breaks when routes shift.

Default for edge traffic
Alternative
DNS steering per resolver

Precise per-request placement and easy weighting. Slow to react to failures, and accuracy depends on where resolvers sit relative to users.

Useful for big video customers
Chosen
Edge plus regional parent tier

Origins see a fraction of the misses, and long-tail content survives in big parent caches. Misses cost one extra hop of latency.

Higher combined hit ratio
Alternative
Flat edges talking to origin

Fewer moving parts and lower miss latency. Origin load scales with the number of PoPs, and every purge triggers 180 refetches.

Does not scale with PoPs
Chosen
Pull on first request

Content enters the cache only when someone asks for it, so disks hold what is actually popular. The first viewer in each region pays a miss.

Fits most workloads
Alternative
Push before release

Pre-warm a game patch or film launch everywhere so the first second is a hit. Great for planned spikes, wasteful for everything else.

Use for known launches
Chosen
Shared cache across customers, isolated keys

Disks are pooled, so busy customers use space quiet ones are not using. Keys include the zone so data never leaks between customers. A noisy customer can evict others, which per-zone quotas limit.

Efficient use of hardware
Alternative
Dedicated cache per customer

Predictable performance for large customers, at much lower utilisation. Usually sold as a premium option rather than the default.

Premium tier only

07

Failure modes

08

Failure modes

🔌
A whole PoP loses power or upstream links

Users in that city lose their nearest site, and any request in flight fails.

Health checks withdraw the PoP's anycast routes, and traffic moves to neighbours within seconds. Neighbours have spare capacity planned for it, and their caches warm quickly because the parent tier still holds the content.
🌊
A volumetric attack aims at one customer

A botnet sends 8M requests a second at a single hostname, more than any one PoP could absorb.

Anycast spreads the flood across all sites. Drop malformed packets in the kernel or NIC, rate limit per source and fingerprint at the edge, and challenge suspicious clients before they reach caches or origins.
☠️
Cache poisoning through an unkeyed header

An attacker sends a request with a crafted header that the origin reflects into the page. The response is cached under the normal key and served to everyone.

Strip or normalise headers that are not part of the key before forwarding, refuse to cache responses that vary on unexpected inputs, and let customers declare which headers the origin may use.
📭
A purge misses some servers

A network partition isolates several servers during a purge, and they keep serving the old version after the customer is told it is done.

Report completion only after every server acknowledges. Servers that lag behind the purge log stop serving the affected zones until they catch up or flush.
🧯
The origin goes down for an hour

As TTLs expire, more requests fall through to a dead origin, and the site slowly turns into error pages.

Serve stale copies with stale-if-error, keep negative results short, and use coalescing so a recovering origin is not hit by every waiting request at once.
📜
Certificates expire

An automation bug stops renewals. Thousands of hostnames will fail TLS on the same day.

Renew 30 days before expiry, alert on any certificate inside 14 days, and keep a second issuing authority ready.

08

Interview tips

09

Interview tips

1

Open with how users find a PoP. Compare anycast and DNS steering in a couple of sentences, then pick one with a reason. It sets up failover and attack absorption.

2

Draw the tiers. Edge, sharded PoP cache, parent tier, origin. Explain what each one saves, using your hit-ratio numbers.

3

Talk about cache keys. Normalising query strings and headers is where real hit-ratio gains come from, and most candidates skip it.

4

Cover stampedes. Request coalescing and stale serving together answer "what happens when a hot object expires?"

5

Treat purge as a distributed system. Sequence numbers, per-server acks, tags and soft purge. Saying "we invalidate the cache" is not enough.

6

Separate control plane and data plane. Say that serving continues on the last good config if the control plane dies.

09

Evolution

11

Evolution

Stage 1: a few regions
DNS steering to a handful of caches

Reverse proxies in three or four regions, chosen by resolver location. A real improvement over one origin for global users.

Stage 2: dozens of PoPs
Anycast and sharded PoP caches

One IP announced everywhere, consistent hashing across servers in each site, and stale serving to protect origins.

Stage 3: origin protection
Regional parent tier and request coalescing

Edge misses go to shields first, purges become tag-based with acknowledgements, and logs stream to customers.

Stage 4: security in the path
WAF, bot defence and attack absorption

The same edge servers inspect traffic, rate limit and challenge, turning the CDN into the front door of the application.

Stage 5: an edge platform
Compute and data at the edge

Customers run code at every PoP and keep small data close to users, so some requests never leave the edge at all.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.