How CDNs Work
A Content Delivery Network is a geographically distributed network of cache servers (called Points of Presence, or PoPs) positioned close to end users. When a user requests a resource, the CDN routes the request to the nearest PoP rather than the origin server. If the PoP has a cached copy (a cache hit), it responds immediately. If not (a cache miss), it fetches from the origin, caches the response, and serves it.
The latency reduction is significant. An API origin in us-east-1 has a round-trip time of ~200ms to a user in Sydney. A CDN edge node in Singapore reduces that to ~15ms. For static assets, images, JavaScript bundles, CSS, fonts, the entire request is served from the edge and the origin is never touched. For dynamic content, CDNs can still help by terminating TLS at the edge (saving one full round trip for the TLS handshake) and compressing the response.
Asset found at edge PoP, origin never contacted
Anycast routing
Most CDNs use anycast IP routing: many edge servers share the same IP address, and the internet's routing protocol (BGP) directs each client to the topologically nearest one automatically. No DNS lookup is needed to find the nearest edge. The routing is transparent to the client, it sends a request to the CDN's IP and receives a response from whatever PoP is closest.
Unicast CDNs (like AWS CloudFront) use DNS-based routing instead: a DNS query for assets.example.com returns the IP of the nearest edge location. This requires a DNS lookup on each new connection but achieves similar geographic routing.
Pull vs push CDN
Pull CDN (most common)
The CDN fetches from origin on the first cache miss and caches the response. No upload step needed. Assets are cached on demand, at the PoPs that actually serve them.
→ Zero setup for new assets
→ Only caches what is actually requested
→ First request per PoP is a cache miss
→ Used by: Cloudflare, CloudFront, Fastly
Push CDN
You explicitly upload assets to the CDN at deploy time. All PoPs are pre-warmed. No cache misses after deploy. Typically used for large media files and software distribution.
→ Zero cache misses after push
→ Requires explicit upload on every deploy
→ Storage cost at every PoP
→ Used by: Rackspace Cloud Files, Bunny CDN
Major providers
| Provider | PoPs | Coverage | Notes |
|---|---|---|---|
| Cloudflare | 300+ | 100+ countries | Anycast network, single IP, nearest PoP serves the request automatically |
| AWS CloudFront | 600+ | 90+ cities | Deep integration with S3, ALB, Lambda@Edge. Regional edge caches as mid-tier |
| Fastly | 70+ | Global | Instant purge (<150ms global propagation), programmable VCL, popular with streaming |
| Akamai | 4,000+ | Global | Largest network. Enterprise focus. Fine-grained control over routing and security |
Cache-Control: Directing CDN Behavior
Cache-Control is the HTTP response header that tells browsers and CDNs how to cache a response. It is a comma-separated list of directives, each controlling a specific aspect of caching behavior. Getting these directives right is the primary lever for CDN cache hit rate and performance.
The most important distinction: some directives apply only to shared caches (CDNs) via the s- prefix, while others apply to all caches including the browser. This lets you cache aggressively at the CDN while controlling what the browser holds locally.
Key directives
Cache this response for 3600 seconds (1 hour). Both the browser and CDN respect this.
Cache-Control: max-age=3600
CDN caches for 86400s (1 day). Browser ignores s-maxage and falls back to max-age. Lets you differentiate CDN TTL from browser TTL.
Cache-Control: max-age=3600, s-maxage=86400
Do not serve a cached response without revalidating with the origin first (via ETag or Last-Modified). Misleading name, it does NOT mean 'no caching'.
Cache-Control: no-cache
Do not cache at all, ever. For sensitive data: bank statements, PII, session-specific pages. Every request goes to origin.
Cache-Control: no-store, private
Response is specific to one user, CDN must not cache it. Browser may cache. Used for authenticated pages, personalized content.
Cache-Control: private, max-age=0
Response may be cached by any cache including shared CDN caches, even if the request was authenticated. Explicit permission for CDN caching.
Cache-Control: public, max-age=600
Serve stale content for up to 60s while fetching a fresh copy in the background. Zero-latency updates, user never waits for revalidation.
Cache-Control: max-age=300, stale-while-revalidate=60
The resource will never change for the duration of max-age. Browser skips revalidation entirely. Use only with content-hashed filenames (app.a3f9c.js).
Cache-Control: max-age=31536000, immutable
Caching strategies by asset type
Versioned static assets (app.a3f9c.js, logo.v2.png)
Cache-Control: public, max-age=31536000, immutable
Content hash in filename = unique URL for each version. Safe to cache forever. Browser and CDN will hold until evicted. Zero revalidation overhead.
HTML pages
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60
Browser always revalidates (content changes frequently). CDN caches for 5 min, serves stale while updating. Invalidate CDN on deploy.
API responses (public, slow-changing)
Cache-Control: public, s-maxage=60, stale-while-revalidate=30
CDN caches for 60s, serves stale while revalidating. Reduces origin load for popular endpoints. Do not cache if response varies by user.
Authenticated API responses
Cache-Control: private, no-store
User-specific data must never be served from a shared CDN cache. A shared cache returning user A's data to user B is a security incident.
Fonts and icons (third-party or self-hosted)
Cache-Control: public, max-age=31536000, immutable
Fonts do not change. Cache forever. Use font-display: swap in CSS to handle the first-load cache miss without layout shift.
Cache invalidation
Phil Karlton famously said there are only two hard things in computer science: cache invalidation and naming things. The CDN layer makes invalidation concrete. When you deploy new code, CDN edges may still serve the old cached version for hours.
Content-hashed filenames (preferred)
Every build produces filenames with a content hash (app.a3f9c.js). Changing a file changes its URL. Old URL stays cached forever; new URL is fetched fresh. Zero invalidation needed.
Explicit CDN purge API
Trigger a purge via the CDN's API at deploy time. Cloudflare purges globally in under 150ms. CloudFront invalidations take 5-30 seconds. Use path patterns (/assets/*) to batch.
Short TTL with stale-while-revalidate
Set s-maxage=60 and stale-while-revalidate=30. Users see stale content for at most 90 seconds after a deploy, with zero explicit purge needed. Trade-off: more origin requests.
Cache-busting query string
Append ?v=<build_id> to asset URLs. Changes on every deploy. CDN treats different query strings as different cache keys. Simple but leaves orphaned cache entries.
What to Put on a CDN
Not everything belongs on a CDN. The rule is: if the response is the same for every user who requests it, and it is safe to store at a shared cache, it belongs on a CDN. If it is personalized, authenticated, or written by the request, it does not. Misconfiguring cache headers on authenticated content is a security vulnerability, not a performance problem.
Cache on CDN
Static assets with content-hash filenames
Immutable, cache forever. app.a3f9c.js never changes.
Images and video (processed or original)
Large payloads benefit most from geo-proximity. CDNs do on-the-fly resizing and WebP conversion.
Fonts
Loaded on every page. Small files, high frequency. Cache once per PoP, serve forever.
Publicly-cacheable API responses
Product listings, exchange rates, public feeds. Short TTL + stale-while-revalidate works well.
Open Graph images and social previews
Crawled frequently by bots. Expensive to regenerate. Cache with a moderate TTL.
Software downloads and large binaries
CDN offloads the bandwidth cost from your origin. Parallel range requests speed up large downloads.
Do NOT cache on CDN
Authenticated API responses
Cache-Control: private must be honored. Serving user A's data to user B is a security incident.
Session-specific HTML (e.g., /dashboard)
Personalized content varies per user. Cannot be shared across users at a CDN edge.
Responses that vary by cookie or request header without Vary header
Without Vary: Cookie, the CDN serves the first cached variant to all users.
Webhooks and inbound event endpoints
POST endpoints that write data cannot be cached. CDNs terminate TLS but must pass through.
Real-time data (WebSocket, SSE streams)
CDNs do not cache streaming connections. Use them for TLS termination only on these paths.
Security headers via CDN
CDNs are the right place to inject security response headers globally. A single transform rule (Cloudflare Transform Rules, CloudFront Response Headers Policy) adds headers to every response from every origin without any application code change. This ensures headers are applied even on error pages, redirects, and third-party origin responses.
| Header | Example value | Purpose |
|---|---|---|
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | Force HTTPS for 2 years. Prevents protocol downgrade attacks. |
| X-Frame-Options | DENY | Prevent embedding in iframes, blocks clickjacking. |
| X-Content-Type-Options | nosniff | Prevent MIME-type sniffing. Browser uses declared Content-Type. |
| Content-Security-Policy | default-src 'self'; ... | Define trusted content sources. Most important but complex to configure correctly. |
| Permissions-Policy | camera=(), microphone=() | Disable browser features not used by your app. |
The Vary header
The Vary header tells the CDN that the response differs based on specific request headers. Without Vary, the CDN caches one response and serves it to all users. With Vary, it maintains separate cache entries per header value combination.
# Response varies by Accept-Encoding (gzip vs br vs none) Vary: Accept-Encoding # Response varies by language, CDN caches one version per Accept-Language Vary: Accept-Language # DANGER: Vary: Cookie creates a separate cache entry per cookie value # Effectively disables CDN caching for that resource # Avoid on public resources Vary: Cookie
Edge Computing: Logic at the PoP
A CDN PoP was historically a passive cache: it either served a cached response or fetched from the origin. Modern CDNs now allow deploying executable code at every PoP. Cloudflare Workers, AWS Lambda@Edge, and Fastly Compute@Edge run JavaScript or WebAssembly inside the CDN's edge infrastructure, milliseconds from the user.
The model is fundamentally different from serverless functions running in a single region. Edge workers run in V8 isolates (not containers), which start in under a millisecond. They execute at the PoP nearest to the user, so a user in Tokyo runs their request through Tokyo infrastructure rather than waiting for us-east-1. The trade-off is a constrained execution environment: no Node.js APIs, limited CPU time, and no persistent state without external bindings.
What to do at the edge
Verify JWTs at the edge before the request reaches the origin. Reject unauthenticated requests in ~5ms without loading the origin server. Redirect unauthenticated users to login.
Platforms: Cloudflare Workers, Lambda@Edge, Vercel Edge Middleware
Assign users to experiment buckets at the edge. Rewrite the URL or modify the response to serve variant A or B. Zero origin round trips for the routing decision.
Platforms: Cloudflare Workers, Fastly Compute@Edge
Inspect the CDN-provided country/region header (CF-IPCountry, CloudFront-Viewer-Country). Redirect EU users to the EU data residency endpoint. Block access from sanctioned regions.
Platforms: All major CDNs, geo headers available natively
Fingerprint requests at the edge before they reach the origin. Check IP reputation, header patterns, and request rates. Return 429 or serve a challenge page without touching the origin.
Platforms: Cloudflare Bot Management, AWS WAF (CloudFront integrated)
Inject headers, modify HTML, add security headers (CSP, HSTS, X-Frame-Options), resize images on the fly. Transform once at the edge for all PoPs simultaneously.
Platforms: Cloudflare Workers, Lambda@Edge (response), Fastly VCL
Render pages at the edge PoP closest to the user using edge-compatible JS runtimes (V8 isolates). Vercel Edge Runtime, Next.js middleware, Cloudflare Pages all support this.
Platforms: Vercel Edge Functions, Cloudflare Pages, Deno Deploy
Edge runtime constraints
No Node.js APIs
Edge runtimes run in V8 isolates, not Node.js. The fs, net, and child_process modules are unavailable. Use the Web Platform APIs (fetch, crypto, ReadableStream) instead.
Cold start is near-zero, but CPU is limited
Isolates start in under 1ms (no container spin-up). But CPU time per invocation is capped (typically 50ms on Cloudflare Workers). Long-running computations do not belong at the edge.
No persistent state (without bindings)
Edge workers are stateless by default. Use KV (eventually consistent), Durable Objects (strongly consistent, but expensive), or D1/Hyperdrive for state that must persist.
Global deployment only
Edge workers deploy to all PoPs simultaneously. You cannot deploy to a single region. Rollouts require feature flags in the code, not selective deployment.
Cloudflare Worker, JWT verification at the edge
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Public routes bypass auth check
if (url.pathname.startsWith("/public/")) {
return fetch(request);
}
const auth = request.headers.get("Authorization");
if (!auth?.startsWith("Bearer ")) {
return new Response("Unauthorized", { status: 401 });
}
const token = auth.slice(7);
const valid = await verifyJWT(token, env.JWT_PUBLIC_KEY);
if (!valid) {
return new Response("Forbidden", { status: 403 });
}
// Forward to origin with verified identity header
const proxied = new Request(request);
proxied.headers.set("X-User-Id", valid.sub);
return fetch(proxied);
}
}