What a CDN actually does
A CDN is not magic and it is not just 'caching'. Here is the mental model, the request lifecycle, the headers that control it, and the four ways teams break it.
Assumes you know
- What an HTTP request is
- That websites are served by a server somewhere
Someone on your team says “we should put a CDN in front of it” and everyone nods. You nod too. Later you look it up and get a sentence like “a geographically distributed network of proxy servers”, which is technically true and explains nothing.
Let’s fix that properly.
The problem a CDN solves
Your server lives in one place. Let’s say a data centre in Virginia.
Your users do not live in one place. Someone in Sydney wants your homepage. Their request has to physically travel to Virginia and the response has to travel back. That is roughly 16,000 km each way, through undersea fibre, at about two-thirds the speed of light.
And it isn’t one trip. Opening a secure connection takes a handshake: a few round trips before a single byte of your actual page moves. So that distance gets paid three or four times over before your user sees anything.
Try it. Pick where your visitor is and watch the same request take two different routes:
Interactive
Where is your visitor?
… ms
… ms
Estimates for a first, uncached request over fibre. Real numbers vary with routing, congestion and how much work the origin has to do. The gap is the point.
Notice what changes and what doesn’t. The server did the same work in both cases. The only difference is how far the answer had to travel.
So what is a CDN, concretely?
A CDN is a company that already owns servers in a few hundred cities. You point your domain at them. Now, when someone requests your site, they reach the CDN’s nearest server rather than yours.
That nearby server is called an A CDN server close to the end user, at the “edge” of the network. Hundreds of these exist worldwide. Also called a PoP, short for Point of Presence.. Your own server is now called the The server that actually generates your content: your app, your API, your S3 bucket. The CDN falls back to it whenever it doesn’t already have what was asked for..
The edge does one of two things with any request:
- It already has a copy of what was asked for → it replies immediately. This is a cache HIT.
- It doesn’t → it fetches from your origin, keeps a copy, and replies. This is a cache MISS.
That’s the whole idea. Everything else is detail about when it’s allowed to keep a copy, and for how long.
The lifecycle of one request
Here is what actually happens on a first visit, step by step.
A request, start to finish
- 1
DNS points at the CDN, not at you
Your visitor's browser looks up techdecoded.dev and gets back an IP address belonging to the CDN, not your origin server. This is the part that makes everything else possible.
dig +short techdecoded.dev 104.21.x.x # a CDN address, not your server
- 2
The request lands at the nearest edge
The CDN uses anycast routing: the same IP address is announced from every one of its locations, and the internet naturally delivers the packet to the closest one. A visitor in Sydney and one in London hit the same IP and reach different buildings.
- 3
The edge builds a cache key
Before it can check whether it has a copy, the edge has to decide what 'the same request' means. By default that's the method plus the full URL, but you can widen or narrow it, and that choice matters enormously.
GET https://techdecoded.dev/logo.svg -> key: GET|techdecoded.dev|/logo.svg
- 4
Cache MISS: nothing stored yet
First visitor of the day. The edge has never seen this key, so it has nothing to serve. It has to go and ask your origin.
cf-cache-status: MISS
- 5
The edge fetches from your origin
This is the slow trip, the one your visitor was going to make anyway. The difference is that it happens once, on the CDN's well-optimised network, instead of once per visitor.
- 6
The origin's headers decide what happens next
Your response comes back with Cache-Control. This is where you, the developer, actually control the CDN. Say it's cacheable for a day, and the edge stores it. Say nothing useful, and it may store nothing at all.
Cache-Control: public, max-age=300, s-maxage=86400
- 7
The edge stores a copy and replies
Your first visitor gets a normal, slightly slow response. They paid the full distance. Someone always does.
- 8
Every later visitor gets the HIT
The next request for that key from anywhere near that edge is answered from local disk or memory. No origin trip, no ocean crossing. That's the payoff.
cf-cache-status: HIT age: 412 # seconds this copy has been cached
Check yourself
Your site is behind a CDN. A visitor in Tokyo loads your logo, then thirty seconds later a different visitor in Paris loads the same logo. What does the Paris visitor get?
The headers that actually control it
This is the part worth memorising, because it is the part you will edit.
| Directive | Who it talks to | What it means |
|---|---|---|
public | Everyone | Any cache may store this, including shared ones like a CDN. |
private | Browser only | The user’s browser may keep it; shared caches must not. Use for anything personalised. |
no-store | Everyone | Never write this to disk anywhere. For genuinely sensitive responses. |
max-age=600 | Browser | Fresh for 600 seconds. After that, ask again. |
s-maxage=86400 | Shared caches | Same idea, but only for the CDN, and it overrides max-age there. |
stale-while-revalidate=60 | Shared caches | Serve the slightly stale copy instantly, and refresh it in the background. |
no-cache | Everyone | Store it, but check with the origin before reusing it. Confusingly, this does not mean “don’t cache”. |
The combination you will use most often:
Cache-Control: public, max-age=60, s-maxage=86400, stale-while-revalidate=600
Read that out loud: anyone may cache this; browsers should re-check after a minute; the CDN may hold it for a day; and if it goes stale, serve the old copy immediately while fetching a fresh one.
The cache key: where most bugs actually live
The edge needs to answer: have I seen this exact request before? The cache key is how it decides. By default it’s roughly the method plus the full URL.
That default is fine until it isn’t. Two failure modes, in opposite directions:
The key is too broad. Different responses collide under one key. Your API
returns different JSON for Accept-Language: en and Accept-Language: fr, but the
URL is identical, so the CDN happily serves French content to English speakers.
The fix is the Vary header, which tells the cache “this response also depends on
that request header”:
Vary: Accept-Language
The key is too narrow. Nothing ever gets shared. If your cache key includes a
tracking cookie or a ?utm_source= parameter, then every visitor generates a
unique key, every request is a MISS, and your hit rate is approximately zero. You
now have all the cost of a CDN and none of the benefit.
Check yourself
Your marketing team starts sending traffic to /pricing?utm_source=twitter, /pricing?utm_source=newsletter, and a dozen other variants. Query strings are part of the cache key by default. What happens?
Getting rid of stale content
You’ve told the CDN to hold your CSS for a year. Then you ship a redesign. Now what?
There are two approaches, and one of them is much better.
Purging. You call the CDN’s API and say “forget /styles.css”. It works, but
it’s an action you have to remember to take, it propagates across hundreds of edges
with a short delay, and if it fails silently your users see the old site.
Fingerprinting. You name the file after its contents:
styles.a3f9c2.css # the old build
styles.7b1e04.css # the new build, different content, different name
The new HTML references the new filename. That’s a URL the CDN has never seen, so it’s a fresh cache key with nothing stale behind it. The old file just sits there until it expires, harming nobody.
What a CDN will not fix
This is the section that saves you a wasted sprint.
- A slow origin. If your homepage takes 3 seconds to generate, every cache MISS still takes 3 seconds. A CDN reduces how often you pay that, not how much it costs.
- Content that can’t be shared. A logged-in dashboard showing someone’s own data must not be cached publicly. Route it through the CDN for the better network path, but don’t cache the HTML.
- A slow first byte from your own code. Database queries, N+1s, cold serverless starts. The CDN never sees these on a HIT, and doesn’t help at all on a MISS.
- Bad caching headers. A CDN in front of an app that sends
Cache-Control: no-storeon everything is an expensive pass-through pipe.
Checking your work
Everything above is visible in response headers. You don’t need a dashboard.
curl -sSI https://techdecoded.dev/ | grep -iE 'cache|age|cf-|x-cache'
What to look for:
cf-cache-status(Cloudflare),x-cache(CloudFront, Fastly), orx-vercel-cache, showingHIT,MISS,EXPIRED,DYNAMIC, orBYPASS.age: seconds since this copy was cached. A rising number across requests means you’re being served the same stored copy.cache-control: what your origin actually sent, which is often not what you thought you configured.
Try it yourself
Catch a CDN in the act
Pick any large site. Most are behind a CDN. Run this twice in a row:
curl -sSI https://developer.mozilla.org/en-US/ | grep -iE 'cache|age'Then try a URL that almost certainly isn’t cached, by adding a random query string:
curl -sSI "https://developer.mozilla.org/en-US/?cachebust=$RANDOM" | grep -iE 'cache|age'Compare the two.
What you should see
On the first pair of requests you’ll typically see a cache status of HIT and an
age header with a non-zero value, proof you’re being served a stored copy rather
than a freshly generated one.
The random query string produces a cache key nothing has ever requested before, so
you should see MISS (or EXPIRED), and age: 0 or no age header at all. You
just forced a trip to the origin, and you can usually feel it in the response time.
That single difference, a query parameter nobody thought about, is the same
mechanism behind the utm_source problem earlier. Now you can see it directly.
A sensible starting configuration
If you’re setting this up for the first time and want defaults that are hard to get wrong:
# Fingerprinted build assets: the filename changes when content changes
Cache-Control: public, max-age=31536000, immutable
# HTML pages: short at the browser, longer at the edge, refreshed in background
Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400
# Anything personalised or authenticated
Cache-Control: private, no-store
Three rules cover most sites. Start there, then look at your hit rate and tune.
Check yourself
Your API returns a user's own order history at /api/orders. It's slow, and someone suggests caching it at the CDN for 5 minutes to speed it up. What's the right response?
Where to go from here
Once the above is comfortable, the next things worth understanding are tiered caching (a middle layer that shields your origin so hundreds of edges don’t all stampede it at once), edge compute (running your own code at the edge rather than just serving files), and cache warming for planned traffic spikes.
But none of those matter until the basics are right: sensible Cache-Control
headers, a cache key that isn’t accidentally unique per visitor, and fingerprinted
assets. Get those three right and you’ve captured most of the value.
If you remember nothing else
- A CDN's main trick is distance: it answers from a server near your user instead of from your origin, and physics does the rest.
- Cache-Control headers are the steering wheel. If you don't set them, you have handed the decision to someone else's defaults.
- The cache key decides whether two requests share a cached copy. Most CDN bugs are really cache key bugs.
- Fingerprinted filenames beat cache purging. Change the URL, and invalidation becomes a non-problem.
- A CDN does not make a slow origin fast; it makes a slow origin matter less often.
Quick answers
- Do I need a CDN for a small site?
- If your users are all in one city and your traffic is low, a CDN changes little. If your users are spread across countries, a CDN is usually the single largest performance win available for the least work, often just changing your DNS.
- Does a CDN work for dynamic, logged-in pages?
- Personalised HTML generally should not be cached publicly, or one user sees another's page. But you can still route it through the CDN for a faster network path, and cache the static assets and public API responses around it.
- What is the difference between a CDN cache and a browser cache?
- The browser cache serves one person and lives on their device. The CDN cache is shared: one visitor's request populates it, and every later visitor near that edge benefits. max-age controls the browser, s-maxage controls the shared CDN cache.
- How do I know whether the CDN is actually caching?
- Look at the response headers. Most CDNs send a status header such as cf-cache-status, x-cache or x-vercel-cache with a value of HIT or MISS, plus an Age header counting how many seconds the copy has been cached.