What a CDN actually does to a request
A user in Tokyo loading a page hosted on a server in Virginia pays for that distance twice: once for the request to arrive, once for the response to come back. Fiber runs at a fixed fraction of the speed of light, and that trip alone costs on the order of 150-200ms round-trip before the origin has done any work. Add a database query and a slow hop or two, and a page that feels instant next door feels sluggish on the other side of the planet.
A CDN, or content delivery network, fixes this for the content that can be copied: images, CSS, JavaScript bundles, video segments, sometimes whole HTML pages. A network of servers positioned close to users keeps copies of that content and answers directly, so most requests never cross the ocean. The origin still holds the authoritative version. The CDN sits in front of it and intercepts as many requests as it can serve from a copy.
How a request picks which server answers
A CDN operator runs servers in many physical locations, usually called Points of Presence, or PoPs: data centers in or near major cities, each holding a slice of the cached content and a fast path back to the origin for everything else. Two mechanisms decide which PoP handles a given request, and large CDNs use both.
DNS-based routing resolves the same hostname to different IP addresses depending on where the query comes from. A DNS query for assets.example.com from a resolver in Frankfurt gets back the IP of a Frankfurt PoP; the same query from SĂŁo Paulo gets a different IP entirely.
Anycast goes further. Many PoPs announce the same IP address over BGP, and ordinary internet routing decides which one a given packet actually reaches, based on network topology rather than geography. The CDN never makes that choice. Failover is faster too: when a PoP drops off the network, routers stop sending it traffic, with no DNS change to propagate.
| |
Either way, the client never talks to the origin directly for cached content. It talks to whichever PoP the routing layer picked, and that PoP either has the content or fetches it once and remembers it. Each PoP is doing the job of a reverse proxy in front of the origin, terminating the client’s connection and deciding what happens next, with a cache attached and thousands of copies running at once across the globe.
Cache hits, cache misses, and how to tell which you got
The first request for a URL through a given PoP is a cache miss. The PoP has nothing stored, so it fetches from the origin, stores a copy according to the rules in the response, and returns it. Every later request for that URL, from any client routed to the same PoP, is a hit: served from the PoP’s storage, no origin involved, until the copy expires or gets purged.
One command tells you which one you got:
| |
A cached response carries a vendor header naming the outcome. Cloudflare adds CF-Cache-Status with values like HIT, MISS, EXPIRED (the TTL ran out, so the PoP re-fetched), REVALIDATED (the origin confirmed the old copy was still good), or DYNAMIC (the CDN decided this response isn’t cacheable at all). Fastly’s equivalent is X-Cache: HIT or X-Cache: MISS. Run the same request twice on a cacheable URL and the second one should flip from MISS to HIT.
| |
age is how many seconds ago the PoP fetched this copy from the origin. Here that is 412 seconds into a 3600-second TTL, so the copy has almost an hour left before the next request triggers a fresh fetch.
How Cache-Control tells the CDN what to cache
The CDN doesn’t guess what to cache. The origin tells it, through the Cache-Control header on each response.
| Directive | What it does |
|---|---|
max-age=N | How long any cache — browser or CDN — may keep the response, in seconds |
s-maxage=N | How long a shared cache (the CDN) may keep it — overrides max-age for the CDN specifically, browsers still follow max-age |
public | Explicitly allows caching even on responses that would otherwise look private (e.g. ones with an Authorization header) |
private | Only the browser may cache this; the CDN must not |
no-cache | Caches are allowed to store it but must revalidate with the origin before serving it again |
no-store | Never cache this anywhere, browser or CDN |
For a static asset with a hashed filename (app.a3f9c1.css), the usual setting is aggressive: Cache-Control: public, max-age=31536000, immutable. The filename changes whenever the content does, so a year is safe. A stale copy of app.a3f9c1.css is a contradiction, because that exact name only ever points to one version of the file.
For HTML or an API response you want cached but fresher, Cache-Control: public, s-maxage=60, max-age=0 holds it at the CDN for a minute while browsers revalidate every time. It is the same short-TTL, cache-aside instinct behind how Redis caching works, enforced by an HTTP header instead of application code.
Get this wrong in the other direction and you leak. Serving a per-user page with Cache-Control: public, max-age=3600 and no Vary on the session cookie means the CDN can hand user A’s cached page to user B.
How to purge a stale CDN cache
Sometimes a TTL isn’t good enough: you shipped a fix and can’t wait an hour for it to reach everyone. Every CDN exposes a purge API or a dashboard action for this. Tell it that a URL, or a whole zone, is invalid now, and the next request is treated as a cache miss no matter how much TTL is left.
The cheaper habit is never needing to purge. Version the filename with a content hash or a query-string bump so each deploy is a new URL with its own cache entry, and the old URL’s stale copy stops being requested at all. Keep purging for what you can’t route around: an incident, a legal takedown, a cache poisoned by a misconfigured Vary header.
When a CDN doesn’t help, and what it costs you
Personalized responses aren’t cacheable in any useful sense. A logged-in dashboard, a cart page, anything keyed to a session — the next request is guaranteed to want different content. Pointing a CDN at those endpoints buys nothing except a false sense of having added caching, and if the cache rules are wrong it actively risks serving one user’s private response to another.
A CDN also adds a layer to debug through. “It works locally but not in production” sometimes means the code is fine and a PoP is serving a response cached from before your fix; the request never reaches the origin until the TTL runs out or you purge it.
Traffic shape matters too. An internal tool, or a regional service with no international audience, pays for PoPs it never uses and recovers latency it never had. A CDN solves two specific problems: the round trip to distant users, and repeated load on the origin. If neither one is yours, it’s infrastructure with nothing to do.
How to check whether you already have a CDN
Run curl -I against a static asset on a site you maintain. If the response carries cache-control, an age header, or a vendor status like CF-Cache-Status or X-Cache, something is already caching in front of your origin, whether you set it up deliberately or your host bundled it. If those headers are missing on assets that get requested from more than one region, that’s the concrete case for adding one: Cloudflare, AWS CloudFront and Fastly all front a domain on a free or usage-based tier in under an hour, and the origin code doesn’t have to change.