Physics sets a floor on latency: light in fiber crosses an ocean in tens of milliseconds, and no amount of server tuning beats the speed of light. A CDN’s core move is to stop making users cross the ocean. It puts a copy of your content near them, and the request never travels far.
A content delivery network is, at heart, one idea applied at planetary scale: a distributed cache in front of your origin. Hundreds of caching servers in Points of Presence (PoPs) around the world, each holding copies of your content, each answering the users nearest to it. Understanding it well is mostly understanding caching — plus the two genuinely hard parts that geography adds.
Hit, miss, and the number that matters
A request arrives at the closest edge node. One of two things happens.
Cache hit: the edge already holds a fresh copy. It serves it immediately — a few milliseconds away, no origin contact. Fast for the user, free for your servers.
Cache miss: the edge has no copy, or its copy has expired. It fetches from your origin, stores a copy for next time, and serves it. Slower for this user, and it costs your origin a request.
The fraction of requests served as hits is the cache hit ratio, and it’s the number that tells you whether your CDN is earning its keep. A 95% hit ratio means your origin sees 1 request in 20 — a 20× load reduction, plus 95% of users getting edge-speed responses. On mostly static content, a 50% hit ratio means something is wrong with your cache configuration, and half your traffic is paying the full round-trip anyway. Chasing hit ratio is most of the operational work of running a CDN well.
The two payoffs follow directly: latency (content comes from nearby) and origin offload (most requests never reach you). The offload is also your best defense against traffic spikes and a big chunk of what a CDN’s DDoS protection actually is — the edge absorbs the flood.
What the origin gets to say: Cache-Control
The edge doesn’t guess how long to keep things. The origin tells it, primarily through the Cache-Control response header. The important directives:
max-age=3600— this is fresh for 3600 seconds; after that, revalidate. Applies to any cache, browser or CDN.s-maxage=86400— a freshness lifetime for shared caches (the CDN) specifically, overridingmax-agefor them. Lets you cache hard at the edge while telling browsers something shorter.no-store— never cache this. For genuinely private or per-request dynamic responses.private— a browser may cache it, but a shared cache (the CDN) must not. For per-user content that’s still safe in the user’s own browser.stale-while-revalidate=60— serve the stale copy right now and refresh in the background. This is the one people underuse: it means a miss/expiry doesn’t make a user wait for the origin — they get the slightly-old copy instantly while the edge quietly fetches the new one.
The TTL (time to live) is just the freshness lifetime these directives set. And revalidation has a cheap form: with an ETag (a content fingerprint), the edge can ask the origin If-None-Match: "<etag>", and the origin replies 304 Not Modified with no body if nothing changed. You confirm freshness without re-sending the bytes — a big saving on large, rarely-changing assets.
The cache key, and how you accidentally destroy your hit ratio
For every request the CDN computes a cache key — the identity under which it stores and looks up a response. By default that’s typically the URL (host + path + often the query string). Two requests with the same key share a cached copy; two with different keys don’t.
This is where hit ratios quietly die. If your cache key includes something that varies per user or per request — a tracking query parameter like ?utm_source=…, a cookie, a User-Agent — then every variation is a separate cache entry, most seen once, and your hit ratio collapses toward zero even though the actual content is identical. Common fixes: strip or ignore marketing query params in the cache key, and be deliberate with the Vary header, which tells the cache to key on specific request headers. Vary: Accept-Encoding is fine and necessary (gzip vs brotli). Vary: User-Agent shatters your cache into thousands of near-duplicate entries. Cache-key hygiene is the highest-leverage tuning you’ll do.
Static is easy; dynamic is where CDNs earn their fee
Caching images, CSS, JS, and fonts is the classic case, and it’s easy because they’re identical for everyone and change only on deploy.
The interesting question is dynamic content — API responses, personalized pages. Some of it is more cacheable than instinct suggests:
- Micro-caching: even a 1–5 second TTL on a hot, expensive endpoint can absorb enormous load. At 1,000 requests/second, a 1-second cache turns 1,000 origin hits per second into about one per edge location — or one in total behind an origin shield — and almost no user notices one-second-old data. This is one of the highest-return tricks there is for read-heavy dynamic endpoints.
- Edge compute: modern CDNs run your code at the edge (Cloudflare Workers, Lambda@Edge, Fastly Compute) — do auth, assemble personalized responses, make routing decisions near the user instead of at a distant origin. The CDN stops being just a cache and becomes a place to run logic.
- Truly per-user, uncacheable content still benefits, because the CDN gives you a warm, optimized, TLS-terminated connection close to the user and a fast backhaul to origin over the CDN’s private network — often faster than the public internet even when nothing is cached.
Invalidation: the genuinely hard part
There are, per the old joke, two hard problems in computing, and cache invalidation is one of them. On a CDN it’s hard for a specific reason: your content exists as copies in hundreds of locations, and changing the origin doesn’t touch any of them. They expire on their own TTL, no sooner.
So a long TTL — great for performance — means stale content can linger worldwide after you’ve updated the source. Two approaches:
Explicit purge. CDNs offer a purge/invalidate API to evict an object everywhere. It works, but propagation speed varies by provider and is never atomic across PoPs, so it’s a poor fit for anything that must update instantly and atomically.
Content-hashed URLs (the preferred answer). Put a hash of the content in the filename: app.a1b2c3.js, styles.9f8e7d.css. Now every version is a distinct, immutable URL. You cache these forever (max-age=31536000, immutable) because they can never change — a change is a new URL. Your HTML references the new hashed URLs on deploy, so you never purge anything; old versions just age out unused. This sidesteps invalidation entirely for static assets, which is why framework build tools like Vite and Next.js fingerprint filenames by default.
The pattern that combines both: hash your immutable assets and cache them forever; keep the HTML that references them on a short TTL (or purge just the HTML). The small, cheap file changes quickly; the big, expensive files are cached hard and never need purging.
A few operational truths
Set Cache-Control deliberately on everything. The default when you say nothing varies by CDN and is rarely what you want. Explicit headers are the difference between a 95% and a 50% hit ratio.
Watch hit ratio like an SLI. A sudden drop usually means a cache-key change (a new query param leaking in, a Vary header someone added) and a matching spike in origin load. It’s a reliability signal, not just a cost metric — the origin overload from a cratered hit ratio is a real outage mode.
Don’t cache errors for long — or Set-Cookie. A 500 cached with a long TTL turns a blip into a sustained outage served from the edge. And caching a response that carries a user’s Set-Cookie at a shared edge can hand one user’s session to another — a genuine security bug, and a reason private/no-store exist.
The origin still has to survive a cold cache. After a global purge or a new deploy, the hit ratio briefly drops and origin load spikes — a small thundering herd. stale-while-revalidate and request coalescing (the edge collapsing many simultaneous misses for the same key into one origin fetch) are what keep that spike from becoming an incident.
The rule worth remembering
A CDN is a cache, so the whole game is hit ratio: cache keys clean enough to share copies, TTLs long enough to offload, and invalidation handled by hashed URLs instead of purges. Get those three right and you serve most of the world at edge speed while your origin barely notices. Get the cache key wrong and you’ve paid for a global network that forwards every request to your origin anyway.
Comments