CDN (Content Delivery Network)
A CDN is a global mesh of caching reverse proxies near end users. When a user requests an asset, they hit the nearest edge instead of your origin server. Faster (less latency), cheaper (less origin bandwidth), more resilient (origin can be down briefly without users noticing for cached content).
Examples: CloudFront, Cloudflare, Fastly, Akamai, Bunny.net.
What’s at a CDN edge
Origin (your servers, one region)
↑ cache miss / origin pull
│
[ POP - London ] [ POP - Frankfurt ] [ POP - Tokyo ] [ POP - SF ] ...
↑ ↑ ↑ ↑
User User User User
POP = Point of Presence. Each POP holds cached copies of assets and serves nearby users with single-digit-millisecond latency.
What a CDN actually does
- Caches static assets (JS bundles, CSS, images, video, fonts).
- Terminates TLS at the edge — handshake completes near the user.
- Compresses and optimizes (gzip/brotli, image resizing, WebP conversion).
- DDoS absorption — edge soaks attack traffic before it hits origin.
- WAF (web application firewall) — blocks SQLi, XSS patterns, bots.
- Origin shield — secondary cache layer that funnels misses, reducing origin requests.
- Edge compute (Cloudflare Workers, Lambda@Edge, Fastly Compute) — run code at the POP.
Cache hit vs miss
- HIT: edge has the asset, serves it immediately. ~1–10ms.
- MISS: edge fetches from origin (or origin shield), caches, then serves. First request pays origin RTT; subsequent requests are HITs.
CDN status often surfaces in headers: X-Cache: HIT, CF-Cache-Status: HIT, Age: 142 (seconds since cached).
A “stampede” happens when an asset expires and 1000 simultaneous requests all miss the cache and hit origin. Mitigations:
- Stale-while-revalidate — serve stale content while fetching fresh in background.
- Origin shield — only one POP fetches from origin; others fetch from shield.
- Request coalescing — CDN dedupes identical concurrent misses into one origin request.
Cache control — the contract
The origin tells the CDN (and browsers) how long to cache and what conditions apply, via response headers.
| Header | Means |
|---|---|
Cache-Control: public, max-age=3600 |
cacheable by anyone for 1 hour |
Cache-Control: private |
cacheable only by the user’s browser, not by a shared cache |
Cache-Control: no-cache |
revalidate every time (use ETag) |
Cache-Control: no-store |
never cache, anywhere |
Cache-Control: s-maxage=86400 |
shared caches (CDN) cache for 24h, regardless of max-age |
Cache-Control: stale-while-revalidate=60 |
serve stale up to 60s while fetching fresh |
Vary: Accept-Encoding, Authorization |
cache key includes these request headers |
ETag: "abc123" |
resource version; client revalidates with If-None-Match |
Last-Modified: ... |
revalidate with If-Modified-Since |
Vary: Accept-Encoding is essential — without it, a gzipped response can be served to a client that doesn’t support gzip (broken page).
Vary: Cookie is dangerous — almost every request has a unique cookie, so cache hit rate collapses to ~0.
What to cache, what not to
| Cache | Don’t cache |
|---|---|
Static assets with content-hashed filenames (app.abc123.js) |
logged-in user pages, anything with PII |
| Public marketing pages | API responses for write endpoints |
| Public API GETs that don’t change often | Set-Cookie responses |
| Images / video | session-specific responses (unless Vary: Cookie is acceptable) |
For SPAs, the standard pattern: cache app.<hash>.js for a year (immutable; URL changes when content changes); never cache index.html.
Cache invalidation
The hard one. Two approaches:
| Strategy | How |
|---|---|
| Versioned URLs (preferred) | rename the file when it changes (app.abc123.js → app.def456.js). Old cached version irrelevant; new URL is a fresh fetch. |
| Manual purge | call CDN API to invalidate /path/.... Slow (seconds-to-minutes propagation), expensive at scale, error-prone. |
Versioned URLs are why every modern build tool (webpack, Vite) emits content-hashed filenames.
For dynamic API responses, use short TTLs and stale-while-revalidate rather than purges.
Origin shield
Two-layer caching: POPs fetch from a single regional shield POP, which fetches from origin. Cuts origin traffic dramatically when you have many POPs (every miss across 200 POPs becomes one origin request, not 200).
Configurable in CloudFront / Cloudflare. For high-traffic origins, basically always enable.
Signed URLs / signed cookies
For paid/private content (videos, downloads):
- Generate a URL with a signed query string (HMAC of path + expiry + account).
- CDN validates the signature at the edge — invalid → 403.
- Lets users access the asset for, say, 1 hour without giving them a permanent URL.
CloudFront and Cloudflare both support this. The signing happens in your app:
# CloudFront example (simplified)
import boto3
from botocore.signers import CloudFrontSigner
signer = CloudFrontSigner(KEY_PAIR_ID, rsa_signer)
signed_url = signer.generate_presigned_url(url, date_less_than=expiry)
CDN as the only entry point
Best practice: lock down origin to only accept requests from the CDN’s IP ranges. Otherwise attackers find the origin’s real IP and bypass the CDN’s WAF/DDoS protection.
- AWS: ALB security group ingress restricted to CloudFront prefix list.
- Cloudflare: Argo Tunnel / Authenticated Origin Pulls.
Edge compute
Run code at the POP for things that need to be near the user but not in your origin:
| Use | Example |
|---|---|
| A/B routing | flip 5% of traffic to a new variant |
| Auth check | verify JWT before letting it reach origin |
| Geo redirect | EU users → eu.example.com |
| Image resizing | ?w=400 returns a resized variant |
| Personalization at the edge | inject a user’s name into a cached page |
Lambda@Edge / Cloudflare Workers / Fastly Compute. Trade-off: cold-start latency, smaller language/runtime support, harder to debug than your main app.
Common interview confusions
- “A CDN is just a cache.” — it’s also TLS termination, DDoS absorption, WAF, edge compute, image optimization. Caching is one feature.
- “Cloudflare and CloudFront are interchangeable.” — operationally similar, but Cloudflare is a network with built-in features (WAF, DDoS, free tier); CloudFront is AWS-tightly-integrated and pay-per-use.
- “
Cache-Control: max-age=3600means it’ll always be cached for an hour.” —max-ageis for browsers/private caches;s-maxageis what shared caches (CDN) honor. Withouts-maxage, CDN may usemax-ageor its own default. Specify both. - “Purging the CDN cache propagates instantly.” — usually 30s–several minutes globally, sometimes longer. Use versioned URLs instead.
Interview angle
- “What does a CDN do beyond caching?” — TLS termination, DDoS absorption, WAF, compression, image optimization, edge compute, signed URLs.
- “How do you cache an asset that changes occasionally?” — content-hashed filename + long
Cache-Control: max-age=31536000, immutable. When the asset changes, the filename changes, so caching is automatic. - “
max-agevss-maxage?” —max-ageis for browsers/private caches;s-maxageoverrides for shared caches (CDN). Specifys-maxageto control CDN TTL independently of browser TTL. - “What does
Vary: Accept-Encodingdo?” — tells the CDN to cache different versions perAccept-Encodingvalue. Without it, a gzipped cached response could go to a client that didn’t ask for gzip. - “Cache stampede — how do you mitigate?” —
stale-while-revalidate, request coalescing at the CDN, origin shield (single layer between POPs and origin). - “How do you serve user-specific content via a CDN?” — generally don’t (cache hit rate dies). Either use signed URLs (per-user authorization, but the bytes are still public-cacheable), or do edge personalization with edge compute, or cache the public shell and load user-specific bits via separate uncached API calls.