important mid · part of Skills & Topics · Senior SWE Roadmap · related: Caching Strategies · Load Balancing · Netflix-like)
What a CDN actually is
A CDN (Content Delivery Network) is a geographically distributed set of proxy servers (“edge nodes” or “PoPs” — points of presence) that cache copies of content close to where users physically are, so a request doesn’t have to travel all the way back to the origin server for every byte.
That’s the mechanism. The reason it works at all is physics, not cleverness.
The latency-physics argument
Light in fiber travels at roughly 200,000 km/s (about two-thirds of c, due to the refractive index of glass). A round trip from New York to Singapore is on the order of 15,000 km each way — that’s a hard floor of ~150ms RTT before any server-side processing, queueing, or TCP handshake overhead is added. TLS alone can add another full round trip (or two, without session resumption). None of this is fixable with a faster server, more bandwidth, or better code — it’s the speed of light, and it doesn’t negotiate.
An edge node 50km from the user instead of 15,000km away turns that floor from ~150ms into ~1ms. This is the entire value proposition of a CDN in one sentence: you cannot optimize away distance, so you eliminate it by moving the data closer to the request.
flowchart LR subgraph Without CDN U1[User in Singapore] -- "~150ms RTT" --> O1[Origin in Virginia] end subgraph With CDN U2[User in Singapore] -- "~1-5ms RTT" --> E[Edge PoP in Singapore] E -. "cache miss only<br/>~150ms, amortized" .-> O2[Origin in Virginia] end
This is also why CDN value scales with geographic spread of your users and request volume relative to origin capacity — a service with users in one city and a nearby origin gets little benefit; a global consumer product gets enormous benefit.
Push vs pull CDN
The two models differ in who initiates getting content onto the edge node.
Pull CDN (origin-pull, lazy)
The edge node caches content the first time it’s requested, then serves subsequent requests from cache until TTL expiry — the same cache-aside pattern from Caching Strategies, just running at the edge instead of in front of a database.
sequenceDiagram participant U as User participant E as Edge Node participant O as Origin Server U->>E: GET /logo.png alt Cache miss (first request from this region) E->>O: GET /logo.png O-->>E: 200 OK + Cache-Control headers E->>E: store in cache per Cache-Control E-->>U: 200 OK (from origin, slow) else Cache hit E-->>U: 200 OK (from edge cache, fast) end
- Pros: zero configuration per-asset — the CDN figures out what’s popular by what gets requested; storage is used efficiently (only actually-requested content is cached).
- Cons: the first request in every region pays the full origin round trip (a cold-cache “cache miss tax”); an asset can be evicted and re-fetched if it’s not popular enough to stay cached, causing unpredictable origin load spikes.
- Default choice for the vast majority of CDN use (CloudFront, Cloudflare, Fastly in typical configuration) — most content, most of the time.
Push CDN
Content is proactively uploaded to edge nodes ahead of any request — you (or a build pipeline) explicitly publish assets to the CDN’s storage.
sequenceDiagram participant D as Deploy pipeline participant CDN as CDN edge nodes (all regions) participant U as User D->>CDN: PUT /app-v42.bundle.js (proactive upload, all PoPs) Note over CDN: content is live everywhere<br/>before any user requests it U->>CDN: GET /app-v42.bundle.js CDN-->>U: 200 OK (guaranteed cache hit, no origin round trip)
- Pros: no cold-cache penalty — every request is a guaranteed hit from the moment content is published; you control exactly what’s on the edge and when.
- Cons: you’re responsible for managing storage, uploads, and consistency across every PoP; wasteful for content that’s rarely requested in some regions (uploaded everywhere regardless of demand); doesn’t handle new or unpredictable content gracefully.
- Used for content that changes rarely, must always be available with zero latency variance, and where the total content footprint is small enough to replicate everywhere — software distribution, a bounded set of video assets, firmware images.
| Pull CDN | Push CDN | |
|---|---|---|
| Who initiates caching | Edge node, on first request | Publisher, ahead of demand |
| Cold-start penalty | Yes, per-region, per-asset | No |
| Storage efficiency | High (only popular content cached) | Low (everything replicated everywhere) |
| Operational burden | Low (mostly automatic) | Higher (explicit upload/sync pipeline) |
| Best for | Large, unpredictable, dynamic content sets (typical web app) | Small, stable, latency-critical content sets |
Static vs dynamic content at the edge
Static content (images, JS/CSS bundles, video segments, downloadable files) is the easy case — content-addressable, identical for every user, cacheable for a long time. This is what CDNs were originally built for.
Dynamic/personalized content (a logged-in user’s dashboard, a personalized feed, an API response that depends on request headers or cookies) is harder — by definition the response differs per user or per request, so a naive edge cache either serves the wrong user’s data (a serious bug/security issue) or can’t cache at all.
Three strategies, roughly in order of how much personalization they can handle:
- Don’t cache it — dynamic requests fall through to the origin every time. Simple, correct, but gets none of the CDN’s latency/offload benefit. Fine when dynamic traffic is a small fraction of total load.
- Cache the shell, fetch the personalized part client-side — serve a static HTML/JS shell from the edge (fully cacheable), then have the client make a separate (uncached, or cached-per-user) API call to fill in personalized data. Common pattern for SPAs.
- Edge compute / edge functions — run actual application logic on the edge node itself (Cloudflare Workers, AWS Lambda@Edge / CloudFront Functions, Fastly Compute@Edge). The edge node can now do things like: read a cookie and route to a personalized cache key, do A/B test bucketing, rewrite a response, or even render a personalized page fragment — all without a round trip to origin. This is the modern answer to “personalized but still edge-cached”: push the decision logic to the edge even when the content can’t be a single shared cached blob.
flowchart TD U[User request] --> E{Edge node} E -->|static asset| C1[(Edge cache)] C1 -->|hit| Resp1[Serve directly, no origin hop] E -->|dynamic, needs personalization| EF["Edge function<br/>(runs at the PoP)"] EF -->|cache key includes user segment/cookie| C2[(Edge cache, per-segment)] EF -->|truly unique, uncacheable| O[Origin server]
The key insight: edge compute doesn’t eliminate the need for a cache, it changes the cache key — instead of one cached response per URL, you can have one cached response per (URL, user-segment) pair, computed and matched entirely at the edge.
Cache invalidation at the edge
This is the hardest problem in CDN design, because unlike a single Redis instance, a CDN might have hundreds of edge nodes worldwide, each independently holding a copy of a cached object.
The purge propagation problem
If you actively purge (invalidate) an asset, that purge command has to reach every edge node that might be holding a stale copy. This takes real time — often seconds, sometimes tens of seconds for a “fully global” purge — and different nodes converge at different times.
sequenceDiagram participant O as Origin / CDN control plane participant E1 as Edge: US-East participant E2 as Edge: EU-West participant E3 as Edge: AP-South participant U1 as User (US) participant U2 as User (India) O->>E1: PURGE /product/42 O->>E2: PURGE /product/42 O->>E3: PURGE /product/42 Note over E1: purge applied (fast, ~1s) E1-->>U1: fresh content Note over E3: purge still propagating (slow, ~15s) U2->>E3: GET /product/42 E3-->>U2: STALE content (purge hasn't landed yet)
During that propagation window, different users in different regions see different versions of the same URL simultaneously — a real consistency problem, not just a theoretical one.
The dominant real-world fix: versioned / fingerprinted filenames
Instead of invalidating app.js in place, build tooling hashes the file’s content into the filename: app.a1b2c3d4.js. When content changes, the filename changes, which means:
- The new file is a brand-new cache key — no purge needed, it’s simply never been cached anywhere, so every edge node fetches it fresh on first request (an ordinary pull-CDN cache miss, not an invalidation).
- The old file (
app.old-hash.js) is still technically “cached and valid” everywhere, but nothing references it anymore — it just ages out via normal TTL/eviction. No propagation delay, no window of inconsistency, no purge API calls. - This is why
Cache-Control: max-age=31536000, immutable(cache for a year, never revalidate) is safe on hashed static assets — the content literally cannot change without the filename also changing.
The tradeoff: this only works for content you control the reference to (you rewrite the HTML/manifest that points to the new filename). It doesn’t help for a stable URL that must serve changing content (e.g., an API endpoint, or a CMS page at a fixed path) — those genuinely need active purge, with its propagation delay accepted as a real, bounded staleness window (same tradeoff as any cache).
Interview framing: when asked “how do you invalidate a cached asset globally,” the strong answer leads with avoid needing to via fingerprinted filenames, then covers active purge as the fallback for the cases that can’t avoid it (with the propagation-delay caveat named explicitly).
HTTP caching headers in depth
This is the actual protocol-level mechanism that CDNs (and browsers) obey — worth knowing cold.
Cache-Control directives
| Directive | Meaning |
|---|---|
max-age=N | Cacheable for N seconds by any cache (browser or CDN) from time of response. |
s-maxage=N | Same as max-age, but applies only to shared caches (CDN, proxy) — overrides max-age for them, lets you set a longer/shorter TTL at the edge than in the browser. |
no-cache | Can be cached, but must revalidate with the origin before serving (via ETag/If-None-Match) — misleadingly named, it doesn’t mean “don’t cache.” |
no-store | Genuinely never cache this, anywhere, not even for revalidation. Used for sensitive data (auth tokens, private account details). |
private | Cacheable only by the end user’s browser, not by shared/intermediate caches like a CDN — used for per-user content that’s fine to cache locally but must never leak into a shared edge cache. |
public | Explicitly cacheable by shared caches even if the request had auth headers (which would otherwise make a cache skip storing it by default in some implementations). |
stale-while-revalidate=N | Serve the (now-stale) cached copy immediately, and asynchronously refetch in the background for N seconds after expiry — user never waits on a cache miss; same idea as the stale-while-revalidate fix for cache stampedes, applied at the HTTP layer. |
stale-if-error=N | If the origin errors on revalidation, keep serving the stale cached copy for N seconds instead of propagating the error — a resilience mechanism as much as a caching one. |
Validation: ETag / If-None-Match (conditional GET)
TTL-based caching (max-age) says “don’t even ask, just assume it’s fine for N seconds.” Validation-based caching says “ask, but make the answer cheap” — the client sends what it already has, and the server replies “unchanged” without re-sending the body.
sequenceDiagram participant C as Client (browser or edge node) participant O as Origin Note over C: max-age expired, needs to revalidate C->>O: GET /style.css<br/>If-None-Match: "abc123" alt Content unchanged O-->>C: 304 Not Modified (no body) Note over C: keep serving cached copy,<br/>refresh its freshness lifetime else Content changed O-->>C: 200 OK<br/>ETag: "def456"<br/>+ full body Note over C: replace cached copy end
ETag is an opaque fingerprint of the content (often a hash); Last-Modified / If-Modified-Since is the older, coarser (second-resolution timestamp) equivalent of the same mechanism. The win: a 304 response has effectively no body, so revalidation is nearly free bandwidth-wise even though it’s still a full round trip latency-wise — which is exactly why stale-while-revalidate (skip the round trip on the critical path entirely) is preferred when latency, not bandwidth, is the concern.
Anycast routing: how a request finds its nearest edge node
A CDN advertises the same IP address from many physical locations simultaneously using BGP anycast. Internet routers, running standard BGP shortest-path selection, naturally route each user’s packets to whichever advertising location is topologically closest (fewest network hops / lowest cost path) — with no DNS tricks and no client-side logic required.
flowchart TD IP["Single anycast IP: 203.0.113.1<br/>(advertised via BGP from N locations)"] IP -.BGP announces.-> PoP1[PoP: US-East] IP -.BGP announces.-> PoP2[PoP: EU-West] IP -.BGP announces.-> PoP3[PoP: AP-South] UserUS[User in New York] -->|routed via BGP<br/>shortest path| PoP1 UserEU[User in London] -->|routed via BGP<br/>shortest path| PoP2 UserAP[User in Mumbai] -->|routed via BGP<br/>shortest path| PoP3
Two things fall out of this that are worth knowing for an interview:
- It’s automatic and fast: no DNS resolution step choosing a region (though many CDNs also use GeoDNS as a complementary/fallback mechanism), and it self-heals — if a PoP goes down, BGP simply stops routing to it and traffic shifts to the next-nearest one, transparently.
- “Nearest” means network-topologically nearest, not geographically nearest — usually correlated but not identical; a user’s actual path can be routed to a PoP that isn’t the closest one on a map if that’s the cheaper/faster route in the internet’s routing tables.
Contrast with GeoDNS (an alternative/complementary approach): the DNS resolver, not BGP, picks which edge IP to hand back based on the resolver’s (or client’s) approximate location — coarser-grained, adds a DNS lookup’s worth of indirection, but doesn’t require BGP anycast infrastructure and gives more explicit control (e.g., weighted traffic splitting for canary rollouts).
CDN for video
Video is the highest-stakes CDN use case in practice — it’s bandwidth-heavy, latency-sensitive (rebuffering is highly visible to users), and the dominant cost line item for any video product at scale. It relies on the same primitives above, applied to segmented/chunked delivery:
- Video is encoded at multiple bitrates/resolutions and split into short segments (2–10 seconds each) via HLS or MPEG-DASH.
- Each segment is just a static, immutable, fingerprintable file (
segment-004-1080p.ts) — meaning it caches at the edge exactly like any other static asset, with the versioned-filename invalidation strategy applying trivially (segments never change once encoded). - The client’s adaptive bitrate player requests the next segment at whatever quality current bandwidth supports — the CDN doesn’t need to know anything about bitrate switching, it’s just serving small cacheable files.
- Popular content (a new episode drop, a viral clip) gets naturally cached at every edge node that sees demand, via ordinary pull-CDN cache-aside behavior — a huge, sudden traffic spike is absorbed by the edge tier instead of hammering the origin/transcoding pipeline.
- For catalog “long-tail” content with low demand in a given region, some CDNs combine this with push-style pre-positioning of popular-but-not-yet-requested content (e.g., a new release, pushed ahead of its premiere time) to avoid a wave of simultaneous cold-cache misses at launch.
See Netflix-like) for how this fits into the full pipeline (upload → transcode → segment → CDN → adaptive playback).
Interview angles
- “Why does a CDN help at all?” — lead with the RTT/speed-of-light argument, not just “it’s a cache.” An interviewer wants to see you understand why distance is the bottleneck, not just that caching exists.
- “How do you invalidate a cached asset globally?” — the strong answer is fingerprinted filenames first (avoids the problem), active purge with an explicit propagation-delay caveat second, and knowing that different edge nodes can transiently disagree during purge.
- “Design YouTube/Netflix” — CDN placement and segment-level caching is a core piece of the answer; be ready to explain why individual video segments, not whole video files, are the cache unit.
- “This response is personalized per user — can we still use a CDN?” — edge compute / edge functions is the senior-level answer; naive “no, dynamic content can’t be cached” is the junior-level one.
- “A user in one region sees old content while another sees new content — why?” — purge propagation delay across edge nodes; a good answer states it’s a bounded, real staleness window, not a bug.
- Know
Cache-Controldirectives precisely — mixing upno-cache(revalidate) andno-store(never cache) is a common, telling mistake.