Courses / System Design Interview / Chapter 10
Module 2 · Building Blocks · Chapter 10 of 39
Content Delivery Networks
Builds on Abstractions That Hide the Network, Availability, Reliability, Scalability and Latency, DNS: How a Name Becomes an Address, Load Balancers and Designing a Key-Value Store. Next up: Unique IDs and Sequencers.
Part 1 · Why Distance Matters
Light is fast, but not fast enough
At 8:00 pm on the first night of its Diwali sale, an online store in Mumbai sends a notification to 20 million phones. Within a minute, shoppers in Guwahati, Chennai and Delhi open the app, and every one of them asks the Mumbai servers for the same 400 KB sale banner, the same scripts and the same product photos. The servers' network links fill in seconds. Pages that loaded in one second now take ten, and checkout, the one request that earns money, waits behind a million copies of a banner. Nothing about that banner is personal. It is one file, sent from one city, over and over.
This sale is the chapter's running example, and it has two problems. The first is distance. Light in optical fibre covers roughly 200 km every millisecond, about two thirds of its speed in a vacuum, and real cables wind through routers instead of running straight. A round trip from Guwahati to Mumbai takes tens of milliseconds; reaching a server on the US east coast takes about 200 ms.
The second is bytes. If 20 million shoppers each load the banner in the first hour, that is 8 TB. Delivery is priced per GB, roughly $0.05–0.10 at list prices (check current prices), so the banner alone is $400–800 of traffic, and for image and video products this egress usually costs more than storing the files.
A content delivery network (CDN) attacks both problems by keeping copies of files on servers close to users. It cannot make the bytes disappear, but it moves them off your own links. The next page measures what distance costs before the first byte even arrives.
Three round trips before the first byte
A 200 ms round trip would not matter if a page needed only one. It needs several before the first byte of the reply arrives, and more for the images, scripts and fonts it pulls in. Ignoring DNS and the server's own work:
| Step | Round trips | Server far away (200 ms) | Edge in your city (10 ms) |
|---|---|---|---|
| TCP handshake | 1 | 200 ms | 10 ms |
| TLS 1.3 handshake | 1 | 200 ms | 10 ms |
| Request and the first byte of the reply | 1 | 200 ms | 10 ms |
| Total before the first byte arrives | 3 | 600 ms | 30 ms |
Round trips1
Server far away (200 ms)200 ms
Edge in your city (10 ms)10 ms
Round trips1
Server far away (200 ms)200 ms
Edge in your city (10 ms)10 ms
Round trips1
Server far away (200 ms)200 ms
Edge in your city (10 ms)10 ms
Round trips3
Server far away (200 ms)600 ms
Edge in your city (10 ms)30 ms
Distance is multiplied by every round trip before the shopper sees anything.
Newer protocols shrink the table, not the gap. HTTP/3 runs over QUIC, a transport on UDP that folds the connection and TLS handshakes into one round trip, so the first byte arrives after about 400 ms from far away or 20 ms from nearby. A lost packet also stalls only its own stream, which helps on patchy mobile networks.
Either way, a nearby server is about twenty times faster for anything it already holds, and no line of your code changes. Which raises the next question: how does a CDN put servers near everyone, and how does a phone find the right one?
Edges and points of presence
A CDN runs edge servers: caching servers placed in many cities, often inside or next to large internet providers' networks. A site full of them in one city is a point of presence (PoP), and a large CDN has hundreds. Your own servers, which hold the real copy of everything, are the origin: for the store, its servers in Mumbai.
Shoppers must reach a nearby PoP without knowing it exists. Two methods do the steering, both met before: CNAMEs and location-based answers in DNS: How a Name Becomes an Address, and anycast in Load Balancers.
| Method | How it steers users | Trade-off |
|---|---|---|
| DNS-based | Your domain is a CNAME for the CDN's host name, and the CDN's DNS answers with a PoP near the user's resolver | Fine control, but location is guessed from the resolver, and cached answers change slowly |
| Anycast | Every PoP announces the same IP address, and internet routing carries each user to a nearby PoP | Fast failover with no DNS caching; "nearby" means close in network terms, not always on the map |
Many CDNs combine the two. Inside a PoP, a load balancer often uses consistent hashing on the URL, from Designing a Key-Value Store, so each file is cached on one or two servers instead of every one, which multiplies how many files the PoP can hold.
Akamai, Cloudflare, Fastly, Amazon CloudFront, Google Cloud CDN and Azure Front Door all work this way. A cloud's own CDN is the easy pick when the origin runs in that cloud; Cloudflare is often chosen for bundled DDoS protection, Fastly for fast purges and code at the edge, Akamai for large media and enterprise traffic.
Part 2 · How a CDN Serves a File
Hits, misses and the origin
When a request reaches an edge, the edge looks in its cache. A hit is served at once. A miss sends the edge to the origin, and it keeps the file for next time. The share of requests served from cache is the hit ratio, the most important number on a CDN dashboard.
Predict before you watch: once the Chennai edge holds the sale banner, does the first Delhi shopper's request hit or miss?
- It misses. Each PoP keeps its own cache, and the Delhi edge has never fetched the banner.
- So a new file costs one origin fetch per PoP, until the origin shield, later in the chapter, turns those into one.
Watch for which requests reach the origin as the sale opens in two cities
Read it as steps
The sale opens in Chennai and Delhi. Watch which requests reach the origin. Timings are illustrative.
Each PoP fills its own cache, so the first shopper in each city pays for a miss and everyone after is served nearby. An expired copy is checked, not blindly downloaded again: the edge asks whether the file changed, and a 304 Not Modified reply carries only headers.
At 100,000 requests a second, a 95% hit ratio leaves 5,000 a second for the origin; 99% leaves 1,000. The last few points of hit ratio cut origin load the most.
Pull or push
In the scene, files reached the edge only when someone asked. That is one of two ways to fill a CDN:
| Pull (origin pull) | Push | |
|---|---|---|
| How | The edge fetches a file the first time someone asks for it | You upload files to the CDN before anyone asks |
| Effort | Almost none: point the CDN at your origin | You manage uploads, updates and deletions yourself |
| First request | A miss in each PoP | Already there |
| Good for | Sites and apps with many small files | Large files you know will be wanted, such as a game patch |
Pull trades one miss per PoP for almost no work; push trades work and storage fees for certainty. Most teams pull, and pre-warm before a known rush: an hour before 8 pm, a script (or the CDN's prefetch feature) requests the banner and the sale page's files through each region, so the rush finds them cached. Push wins when files are so large and so predictable that even one miss per PoP would hurt the origin.
Designing the cache key
Once files are at the edge, the edge must decide when two requests want the same copy. It stores each response under a cache key, by default the host name, the path and the query string. The rule for a good key is short: it holds everything that changes the response, and nothing else. Both directions of mistake are common:
- Too specific, and the hit ratio collapses.
?utm_source=whatsappand?utm_source=instagrammake two copies of the same sale page, each missing separately. Strip tracking parameters, sort the rest so?size=m&color=redmatches?color=red&size=m, and lower-case the host. - Too broad, and users get the wrong thing. If the key ignores language, the Hindi sale page can reach a shopper who asked for Tamil.
- Cookies usually mean personal. By default, many CDNs will not cache a response that sets a cookie. Serve static files from a separate, cookie-free domain so their requests never carry the login cookie.
The two mistakes fail differently. A key that is too specific shows up on the dashboard as a falling hit ratio and a busy origin. A key that is too broad shows up as complaints, because it serves the wrong content quietly, which makes it the worse of the two.
Path and query are the easy part. The origin can also add request headers to the key, and that is where the costliest mistakes hide.
The Vary header and its traps
The origin adds a request header to the cache key with the Vary response header. Vary: Accept-Language tells every cache to keep a separate copy for each value of that header. On a header with a few values, that is exactly right; on a header with thousands, it quietly switches caching off. Here is how the common choices behave:
| Vary on | Values browsers send | Result |
|---|---|---|
| Accept-Encoding | A few after normalising: Brotli, gzip, none | Fine: most CDNs normalise it, so 2–3 copies |
| Accept-Language | Hundreds of spellings, such as en-IN,en;q=0.9,hi;q=0.8 | Fine only if the edge first maps it to the languages you serve |
| User-Agent | Thousands: one per browser version and device | The hit ratio collapses; some CDNs refuse to cache it at all |
| Cookie | One per shopper, once any login or tracking cookie is set | Every shopper gets a private copy, so nothing is shared |
The pattern: vary only on a header the edge has reduced to a handful of values.
When mobile and desktop pages truly differ, have the edge sort requests into a small device class, such as mobile or desktop, and key on that. The same trick serves an A/B test: the edge assigns each shopper to group A or B and keys on the group, not on the cookie that carries it.
The key decides which copy a request gets. The next question is how long that copy may be served.
TTLs and Cache-Control headers
How long a copy stays fresh is its TTL (time to live). The origin sets it with response headers, so the caching policy lives next to your code. Three of the store's responses:
GET /static/app.3f9a2c.js -> Cache-Control: public, max-age=31536000, immutable GET /sale/index.html -> Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=30, stale-if-error=86400 GET /api/cart -> Cache-Control: private, no-store
A year (31,536,000 s) for a file whose name changes with its content, a minute at the edge for the page that links to it, never for a shopper's cart.
s-maxage applies only to shared caches such as the CDN, and overrides max-age there; browsers ignore it. So browsers check the sale page every time, which the nearby edge answers with a cheap 304, while the edge keeps its own copy for 60 seconds. You can purge a CDN, but you cannot purge a browser's cache, so give browsers the shorter TTL.
The last two directives help when the origin struggles. stale-while-revalidate lets the edge serve the old copy for 30 more seconds while it fetches a fresh one in the background, so no shopper waits for the refresh. stale-if-error lets the edge keep serving the last good copy for a day if the origin is failing. Support varies by CDN, but where it exists, an origin outage shows slightly old pages instead of errors.
If a page with a name, address or cart is cached publicly, the next shopper who requests that URL from the same PoP sees it. Mark personal responses private or no-store, and test the headers before every launch.
Versioned file names
A year-long TTL raises an obvious question: how does a changed script ever reach shoppers? The answer is to never change a file under the same name. Each build puts a hash of the file's content into its name, so app.3f9a2c.js becomes app.4b1e7d.js when the code changes. A given name never changes, so a year-long TTL is safe. The HTML that references the files gets a short TTL, such as a minute, and a deploy reaches every shopper within a minute without a single purge.
- 1Buildapp.js gets a new hash in its name: app.4b1e7d.js
- 2UploadNew files go to the origin; old files stay for a few days
- 3Switch the HTMLThe sale page now references app.4b1e7d.js
- 4Within 60 secondsEdges refresh the HTML, and new visitors get the new script
- 5Pages already openThey keep working, because app.3f9a2c.js still exists
The last step is why old files stay for days: a shopper who opened the page at 7:29 still asks for the old script, and deleting it would break their page mid-sale. Versioning also makes rollback instant, because the previous HTML still points at files that exist.
Versioned names make deploys cheap, but each new file still costs one miss in every PoP. The next page removes most of those.
Origin shields
With 50 PoPs, every new file and every expiry of the sale page can cost 50 separate misses, all landing on the origin at 8 pm together. An origin shield is a middle layer of cache: every edge sends its misses to one shield location, usually close to the origin, and only the shield talks to the origin. Fifty misses become one.
The shield helps most with the long tail: the store's 2 million product photos, each requested too rarely for every PoP to keep. Suppose edges hit 90% of requests and the shield hits 80% of what reaches it. The origin then sees 10% × 20% = 2% of requests instead of 10%: a fifth of the load, from one extra layer.
The price is one extra hop, paid only on misses: the edge-to-shield leg adds a few to tens of milliseconds. The shield also concentrates load, so it must be sized for every PoP's misses. And the design must say what happens if the shield location fails: edges usually fall back to the origin, so the origin must survive that for a while.
A shield also makes the riskiest CDN operation, the purge, far gentler, as the next page shows.
Purging without hurting the origin
At 8:10 pm, the store notices a TV listed at ₹4,999 instead of ₹49,999. Checkout reads prices from the origin, so no order goes through at that price, but the product page is cached for 10 minutes and thousands of shoppers are seeing it. So the team purges: it asks the CDN to drop the page from every PoP and the shield. A purge reaches every PoP within seconds on some CDNs and minutes on others. The order of steps matters more than the purge itself.
Watch for what the edges fetch after each purge, and where they fetch it from
Read it as steps
A purge before the fix refills every edge with the mistake; a purge after the fix refills them through the shield with one origin fetch.
Fix the origin before purging, or the edges refetch the mistake within seconds. The shield then turns the purge's 50 misses into one origin fetch, which matters most when a mass purge drops thousands of files at once.
Two refinements help. A soft purge marks copies stale instead of deleting them, so edges keep serving the old copy while one request revalidates it; use it for routine updates, and a hard purge for content that must not be seen again. A tag-based purge drops every response carrying a tag: tag each page showing the TV with product-8812, and one purge clears the product page, the sale listing and search results together.
Part 3 · Beyond Static Files
What belongs on a CDN
Purges are for mistakes; most content should simply be cached or not. Ask two questions of every response: is it the same for everyone, and can it be a little old? If both answers are yes, it belongs on the CDN. Here is how the store's responses sort out:
| Content | On a CDN? | Why |
|---|---|---|
| Scripts, styles, fonts, icons | Yes, with versioned names and long TTLs | The same for everyone, requested on every page |
| Banners and product photos | Yes | Most of the bytes; many CDNs also resize and compress them |
| Video ad segments | Yes | Streaming video is a series of small files, ideal for caching |
| The public deal and stock API | Yes, for a second or two | The same for everyone, and a second old harms no one |
| Cart, orders, addresses | Pass through, never cached | Different for every shopper; caching would leak data |
| Writes: orders, payments, reviews | Pass through to the origin | Nothing to cache; the edge only forwards them |
Everything shared and slightly stale goes to the edge; everything personal or written goes through.
Three cases look cacheable but are not worth it. The deep long tail, such as a product page viewed once a month, almost never hits, so a cached copy buys little. The price at checkout must come from the origin, even if the product page showed a cached one. And some data must, by law or contract, stay out of third-party caches entirely.
The deal API row hides the most useful trick in the chapter, and it gets the next page.
Micro-caching live data
The sale's lightning deal page shows a countdown and the stock left, and a million shoppers keep it open, each polling every 2 seconds: 500,000 requests a second for one response. Cache it for just 1 second at each of 50 PoPs, and each PoP asks the origin about once a second. The origin sees about 50 requests a second instead of 500,000. This is micro-caching.
It works only if, when a copy expires, the edge sends one request to the origin and makes everyone else wait for that answer. This is request collapsing. Without it, the hundreds of requests that arrive in the instant after expiry all miss together and reach the origin at once, every second.
The price is a stock count up to a second old. That is fine for display, because the order itself checks stock at the origin with a conditional update, the "fresh read is not a lock" rule from Abstractions That Hide the Network. The trick flips for anything personal, and for anything whose staleness costs money on its own.
Micro-caching serves shared live data. Checkout is neither shared nor cacheable, yet a CDN still speeds it up.
Speeding up what cannot be cached
A shopper in Guwahati checking out against a far-away origin pays about three long round trips before the first byte. Behind a CDN, the same uncacheable request is faster, through dynamic content acceleration:
- TLS ends at the edge. The handshakes run with the nearby edge, and the edge forwards the request over a warm, already open connection to the origin. From 200 ms away, roughly 600 ms becomes a little over 200 ms.
- Better paths. The CDN carries traffic over its own well-connected network instead of whatever route the public internet picks.
- Small logic at the edge. Redirects, A/B group assignment or a token check can run on the edge, so some requests never travel to the origin.
So even an origin that serves only personal pages gains from a CDN in front. The edge in front of everything also has one more job: keeping attackers out.
Part 4 · Security and Failure
The edge as a shield
At 8:00 pm the store's real shoppers arrive together with bots: scripts that grab deals and floods meant to knock the site over. Because the CDN sits in front of everything, it is the first line of defence:
- DDoS absorption. With anycast, an attack flood spreads across hundreds of PoPs with enormous combined capacity, instead of landing on a few origin servers.
- Filtering. Firewall rules and rate limits at the edge drop bad requests before they cost you anything; Rate Limiters covers the algorithms.
- TLS at the edge. The CDN holds your certificate and encrypts close to users. Keep the leg from the edge to the origin encrypted too.
- Signed URLs. For paid videos or private files, the origin issues links with a signature and an expiry time, and the edge refuses any link without a valid one.
If attackers find the origin's address, they go around the CDN entirely. Accept traffic only from the CDN's address ranges, or require a secret header that only the CDN sends.
What goes wrong
Most CDN trouble comes from a few repeat mistakes. Here they are with what users see and the fix:
| Problem | What users see | Fix |
|---|---|---|
| Old files after a deploy | A new page with an old script, and broken buttons | Versioned file names and a short TTL on HTML |
| Personal data cached publicly | One shopper sees another's cart or address | private or no-store on personal responses |
| Cache key too specific | A low hit ratio, slow pages and a busy origin | Strip tracking parameters, sort query strings, never vary on User-Agent |
| Mass purge or cold cache | A spike of misses that overloads the origin | An origin shield, request collapsing, soft purges in stages |
| Origin down | Errors as soon as cached copies expire | stale-if-error, and longer TTLs for content that rarely changes |
| CDN outage | The site is down although the origin is fine | A second CDN behind DNS failover, and an origin sized for some direct traffic |
The second row does the most damage, because it leaks private data instead of only slowing pages. Treat is this response personal? as a question every endpoint answers before launch, not after a shopper reports seeing a stranger's address.
Track the hit ratio by requests and by bytes, origin requests per second, and edge errors per PoP. A hit ratio that falls right after a release is usually a cache-key mistake.
In the interview
As soon as images, video or a spread-out audience appear in the requirements, bring in the CDN and say exactly what goes on it.
- Say
- "Scripts and images get versioned names and a one-year TTL; the HTML gets 60 seconds at the edge, so a deploy reaches everyone within a minute with no purge."
- Say
- "The deal API is the same for everyone, so the edge caches it for 1 second and collapses misses: 500,000 requests a second become about 50 at the origin."
- Say
- "Cart and checkout are
private, no-store. They give up caching, but TLS still ends at the nearby edge, which saves about two long round trips." - Avoid
- "Put everything on the CDN." Personal pages and writes must pass through, or one shopper sees another's cart.
- Avoid
- "We just purge when something changes." A purge before the fix refills every edge with the mistake, and a mass purge sends every PoP's miss to the origin at once.
Wrap-up
| Term | Meaning |
|---|---|
| Edge server, PoP | A caching server near users; a PoP is a site full of them in one city |
| Origin | Your own servers, which hold the real copy of everything |
| Hit ratio | The share of requests the edge serves from its cache |
| Cache key | What a response is stored under: host, path, query and any Vary headers |
| TTL, s-maxage | How long a copy stays fresh; s-maxage sets it for shared caches only |
| Purge | Telling every cache to drop a response before its TTL ends |
| Origin shield | A middle cache layer, so a miss in every PoP becomes one origin fetch |
| Request collapsing | One origin fetch shared by many simultaneous misses for the same key |
Next: Unique IDs and Sequencers. Every order, message and post needs an ID that no other server will ever hand out, and doing that across fifty servers without a bottleneck is harder than it looks.
Part 5 · Check yourself
Quick check
Q1The store's dashboard shows a 97% hit ratio by bytes but only 70% by requests. Why can the two differ so much, and which one tells you about origin load?
Think it through, then check.
- Big shared files (photos, banners, video) cache well; many small requests, such as API calls and personal pages, pass through.
- Bytes track bandwidth and the egress bill; requests track origin work, and here 30% of calls still reach it.
Q2The sale notification links to /sale?ref=<shopper id> to track clicks. At 8 pm the page's hit ratio falls from 95% to 10%. Why?
Think it through, then check.
- The query string is in the cache key, and every link has a different
ref, so each visit misses on a key nobody else shares. - Fix: drop
reffrom the cache key at the edge but still forward it for analytics, or record clicks with a separate small request.
Q3A sale's 400 KB banner is requested 50,000 times a second. What bandwidth does the origin need at a 99% hit ratio, at 95%, and with no CDN?
Think it through, then check.
- Total demand: 50,000 × 400 KB = 20 GB a second, about 160 Gbps.
- At 99%, the origin serves 500 requests a second: 200 MB a second, about 1.6 Gbps. At 95%, it serves 2,500: about 8 Gbps.
- With no CDN, it needs all 160 Gbps: the network cards of 7 to 16 servers at 10–25 Gbps, running flat out.
Q4Flash prices change on the hour and must show within 5 seconds. A 5-second TTL, or a purge at every change? What does each cost?
Think it through, then check.
- A 5-second TTL is simple and caps staleness; through a shield, a busy page costs the origin one fetch per 5 seconds.
- Purging allows long TTLs, but a purge may take over 5 seconds to reach every PoP, and it empties caches at :00, the peak.
- Better, since changes are scheduled: treat each hour's prices as a new versioned file, such as
/prices/2000.json, pre-warmed just before the hour, and switch pages to it on the hour.
Q5From DNS: How a Name Becomes an Address: you switch CDNs by changing your CNAME, and hours later some users still reach the old one. Why, and how should the switch be planned?
Think it through, then check.
- Resolvers keep the old CNAME answer until its TTL runs out, so users move only as those caches expire.
- Beforehand, lower the record's TTL to 60–300 seconds and wait out one old TTL before the change.
- Keep the old CDN serving until its traffic drains, then turn it off.