How CDNs Actually Work: Edge Caching, TTLs, and the Invalidation Problem

A CDN is a globe-spanning cache in front of your origin. The wins are latency and offload; the hard parts are cache keys, TTLs, and knowing when to invalidate.


How a CDN works: users hit a nearby edge Point of Presence; a cache hit is served locally and fast, a miss is fetched from the distant origin, stored with a TTL, and served — with a Cache-Control header cheat sheet

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, overriding max-age for 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.

Frequently asked questions

What does a CDN actually do?

A CDN (content delivery network) is a large set of caching servers spread across many geographic locations that sit in front of your origin servers. When a user requests content, the nearest CDN edge node serves it if it has a fresh copy (a cache hit); otherwise it fetches from your origin, stores a copy, and serves it (a cache miss). The two benefits are lower latency, because content comes from a nearby node instead of a distant origin, and origin offload, because most requests are answered at the edge and never reach your servers.

What is the difference between a cache hit and a cache miss?

A cache hit is when the edge node already has a fresh copy of the requested content and serves it immediately without contacting the origin — fast and cheap. A cache miss is when the edge has no copy, or its copy has expired, so it must fetch from the origin before responding, which is slower and adds load to your servers. The fraction of requests served as hits is the cache hit ratio, and it's the single most important number for judging whether a CDN is configured well.

How do TTL and Cache-Control headers control CDN caching?

The origin tells caches how long content stays fresh using the Cache-Control header. max-age sets the freshness lifetime in seconds for any cache; s-maxage overrides it for shared caches like the CDN specifically. no-store forbids caching entirely (for private or dynamic responses), private allows browser caching but not CDN caching, and stale-while-revalidate lets the edge serve a slightly stale copy while refreshing in the background so no user waits on the miss. ETag plus If-None-Match enables lightweight 304 revalidation without resending the body.

Why is cache invalidation hard on a CDN?

Because your content lives in copies scattered across hundreds of edge locations, and updating the origin doesn't touch those copies — they expire on their own TTL. If you set long TTLs for performance, stale content can persist worldwide after you've changed it. The standard fix is content hashing in URLs (app.a1b2c3.js), which makes every version a distinct, immutable URL so a deploy references new URLs instead of needing old ones purged. Explicit purge APIs exist but propagate with a delay, so hashed URLs are preferred for anything that changes.

Comments