You cannot beat the speed of light, so you move the data closer instead. The single highest-impact change available for a globally-used product.
Prerequisites: Networking 101, HTTP & TLS Time to read: ~18 minutes
Your servers are in Virginia. Your user is in Karachi.
Round trip Karachi → Virginia: ~230 ms (physics; not negotiable)
A page needs: DNS + TCP + TLS + HTML + 40 assets
Cold load, HTTP/1.1-ish behaviour:
DNS ~50 ms
TCP 230 ms
TLS 230–460 ms
HTML 230 ms
Assets several more round trips
────────────────────────────
Well over 2 seconds — with a server that responds in 5 ms
99% of that time is distance. No code change touches it.
And there’s a second problem: bandwidth. Serving 2 MB images at 1 Gbps caps one server at ~62 requests/second (Latency Numbers). A viral post would saturate your entire fleet with images alone.
A CDN solves both, and it’s usually the cheapest large win in a system design.
A CDN is a global fleet of caching reverse proxies — PoPs (Points of Presence) — in hundreds of cities. Users hit the nearest one. It serves from cache, or fetches from your origin and caches for next time.
flowchart LR
U1[User in Karachi] --> P1[PoP Karachi<br/>~10 ms]
U2[User in Lagos] --> P2[PoP Lagos<br/>~15 ms]
U3[User in Berlin] --> P3[PoP Frankfurt<br/>~8 ms]
P1 -.cache miss.-> S[Shield / regional cache]
P2 -.cache miss.-> S
P3 -.cache miss.-> S
S -.miss.-> O[(Origin<br/>Virginia)]
Users are routed to the nearest PoP by anycast — the same IP address announced from every location, with BGP routing each user to the closest one. Not DNS. This matters: anycast reroutes in seconds when a PoP fails, with no TTL to wait out. → DNS
📐 What it actually buys you:
Without CDN: 230 ms RTT × 4 round trips = ~900 ms + transfer
With CDN: 10 ms RTT × 4 round trips = ~40 ms + transfer
Cache hit rate 95% → origin sees 5% of traffic
Bandwidth: the CDN serves the terabytes, you serve the misses
The TLS handshake benefit is underrated. Even for content the CDN can’t cache, terminating TLS 10 ms away instead of 230 ms away saves 2–3 round trips (~500 ms) on every new connection. The CDN then reuses a warm, long-lived connection to your origin. This is why putting dynamic content behind a CDN still helps.
Images, CSS, JS, fonts, video. Immutable and identical for everyone.
The pattern that makes this work is content hashing:
/static/app.a3f5c9e2.js ← the hash is in the filename
Cache-Control: public, max-age=31536000, immutable ← cache for a year
Deploy new code → new hash → new URL → the CDN fetches it fresh. You never invalidate anything, because the URL changes when the content does. This is the single most useful caching pattern in web development and it’s worth stating explicitly in an interview.
Profile photos, uploaded images, video. Cache aggressively; use versioned URLs when they change.
For private media (a photo only friends can see), use signed URLs — a time-limited, cryptographically signed link the CDN validates without calling your origin:
https://cdn.example.com/photo/abc.jpg?expires=1735689600&signature=a3f5...
The CDN checks the signature and expiry itself. Your origin authorizes once when issuing the URL, not on every fetch. This is how S3 pre-signed URLs and CloudFront signed URLs work, and it’s the standard answer to “how do you serve private media at scale?”
Anything identical across users and tolerant of staleness:
Cache-Control: public, max-age=60, stale-while-revalidate=300
stale-while-revalidate is excellent and underused: serve the stale copy instantly while
refreshing in the background. Users never wait for a cache miss.
🚨 The classic catastrophe: caching a personalized response at a shared cache. One user’s account page gets cached and served to everyone.
Rules that prevent it:
Cache-Control: private or no-store for anything user-specific.Vary: Authorization, Cookie if the response genuinely differs by header./api/me/*), so a
misconfiguration can’t leak.Anything with Set-Cookie for a session, anything containing PII at a shared cache, payment flows,
admin interfaces.
The hard part, as always with caching.
| Method | Speed | Notes |
|---|---|---|
| TTL expiry | Bounded by TTL | Simplest; just wait. Good default. |
| Purge by URL | Seconds–minutes | Explicit API call. Works, doesn’t scale to thousands of URLs. |
| Purge by tag/surrogate key | Seconds | Tag responses (Surrogate-Key: product-42 category-shoes), purge by tag. The best mechanism — Fastly popularized it. |
| Purge everything | Minutes | Nuclear. Every PoP refills from origin at once — a self-inflicted thundering herd. Avoid. |
| Versioned URLs | Instant | No invalidation at all. Best when you can use it. |
🎙️ “I’d use content-hashed URLs for static assets so no invalidation is ever needed, and surrogate-key tagging for API responses so updating a product purges exactly the pages that reference it.”
A CDN doesn’t just accelerate; it shields.
Cache miss consolidation (request collapsing). 10,000 users request an uncached video segment simultaneously. Without collapsing, the PoP makes 10,000 origin requests. With it, one request goes to origin and 9,999 wait for the result. Every serious CDN does this, and it’s the reason a CDN survives a traffic spike that would kill your origin.
Tiered caching / shield PoPs. Instead of 300 PoPs each missing to origin, misses go to a small number of regional shields first. Origin sees far fewer requests, and the shield’s hit rate is much higher because it aggregates demand from many PoPs.
Origin cloaking. Lock your origin’s firewall to the CDN’s IP ranges only. Now a DDoS can’t target you directly — it has to go through infrastructure built to absorb it.
🚨 A common real-world mistake: deploying a CDN but leaving the origin publicly reachable. Attackers
find the origin IP (via DNS history, certificate transparency logs, or an unproxied subdomain like
mail.example.com) and bypass all your protection. If you use a CDN for DDoS protection, the
origin must be unreachable except through it.
Modern CDNs run your code at the PoP: Cloudflare Workers, Lambda@Edge, Fastly Compute.
Genuinely good uses:
?width=400), so you store one originalConstraints: short execution limits, small memory, no persistent local state, limited language support (usually JS/WASM), and cold starts (though far smaller than Lambda’s). It’s for request shaping, not for your application.
⚖️ Edge compute genuinely improves latency, but it’s also a new place for logic to hide. Debugging “why did this behave differently for that user” gets harder when the answer is in a worker running in Lagos.
| Gain | Cost | |
|---|---|---|
| CDN for static | Massive latency and bandwidth wins; usually cheaper than origin egress | Almost none. Do it. |
| CDN for API responses | Fewer origin requests, lower latency | Staleness; real risk of caching private data |
| Long TTLs | Higher hit rate, less origin load | Slow to update; needs an invalidation strategy |
| Purge-all | Simple | Thundering herd on origin; refills every PoP |
| Edge compute | Sub-10 ms logic near users | Execution limits; logic scattered across the edge |
| Multi-CDN | Redundancy; no single vendor outage | Complexity, cost, inconsistent behaviour between vendors |
1. Measure the difference. Put a static file behind a free Cloudflare account. Then:
# Uncached origin
curl -w 'total: %{time_total}s\n' -o /dev/null -s https://origin.example.com/big.jpg
# Via CDN — run twice, first is a MISS, second a HIT
curl -sI https://cdn.example.com/big.jpg | grep -i 'cf-cache-status\|age'
curl -w 'total: %{time_total}s\n' -o /dev/null -s https://cdn.example.com/big.jpg
2. See the headers that control everything.
curl -sI https://cdn.jsdelivr.net/npm/react@18/package.json | grep -i 'cache-control\|age\|x-cache'
Find a site using stale-while-revalidate and one using immutable. Read the values and work out
what update strategy each implies.
3. Break it deliberately. Serve a personalized response (include a username) with
Cache-Control: public, max-age=60 and no Vary. Hit it as two different users through a caching
proxy. Watch one user see the other’s name. This is a five-minute exercise that will make you
paranoid about Vary forever, which is the correct state to be in.
Cache-Control: public on an authenticated endpoint?