Filter concepts by levelShowing all levels.

System Design · Section 26

CDN and Edge Caching

Level
intermediate
Read
20 min
Concepts
3

A CDN caches content at edge locations geographically close to users, cutting both network latency and origin load for anything explicitly marked cacheable — classically static assets, but also dynamic responses that are identical across many requesters. Cache-Control headers (max-age, s-maxage, private, no-store), explicit invalidation or filename versioning, and signed URLs give an origin fine-grained control over what a CDN caches, for how long, and who can access it. The deciding question for cacheability is whether a response is truly identical for every requester — a response with even one embedded personalized field is not safe to cache as a whole.

System Design overview

What is true here

  1. A CDN serves content from edge locations near users, reducing both latency and origin load — for explicitly cacheable content.
  2. max-age sets freshness for all caches; s-maxage overrides it specifically for shared caches like CDNs.
  3. TTL alone bounds staleness — propagating an urgent change needs explicit invalidation or filename versioning.
  4. Cacheable content must be identical for every requester; a single personalized field embedded in an otherwise-shared response is a privacy leak once cached.

What you will be able to do

  • Explain why CDN latency wins come from geographic distance, not server speed
  • Configure Cache-Control headers correctly for shared vs private content
  • Choose invalidation or filename versioning appropriately for an urgent content change
  • Identify content that looks cacheable but contains a hidden personalized field

What a CDN does, and how it is controlled

Why edge caching reduces latency and origin load, and the Cache-Control/invalidation/signed-URL mechanics that control it.

CDN caching for static assets and cacheable responses

corebeginner

A CDN (content delivery network) is a globally distributed set of caching servers, positioned close to end users geographically, that cache and serve content on behalf of an origin server. It removes both the network distance and the origin's repeated-serving cost for anything that can be safely cached — classically images, CSS and JS, but also API or page responses that are the same for many users.

Think of it as

An origin server is a single central warehouse; a CDN is a network of local pop-up stores near where customers actually are. A customer who wants something the local store already stocks gets it immediately, without a truck driving all the way from the warehouse. Only when the local store doesn't have an item does it need to ask the warehouse — and once it does, it stocks that item locally for the next customer.

text
Client → CDN edge (near client)
           ├─ cache hit  → serve from edge, fast
           └─ cache miss → fetch from origin,
                            cache it, then serve

What we're doing: Show the latency difference a CDN edge cache makes for a globally distributed user base.

cdn-latency.txttext
Origin server: single region, us-east.

Without a CDN:
  User in Tokyo requests a product image.
  Round trip to us-east: ~180ms network latency
  alone, before the origin even processes the request.

With a CDN:
  User in Tokyo requests the same image.
  CDN edge in Tokyo already has it cached (a prior
  user's request populated it): ~5ms round trip to
  the nearby edge, no trip to us-east at all.

Origin load drops too: that image is now served from
the Tokyo edge for every subsequent nearby request,
not re-fetched from the origin each time.
5
This is the pure network-distance cost a CDN exists to remove — nothing to do with the origin being slow, just physical distance.
10
The edge cache serving locally is what collapses that 180ms round trip down to a few milliseconds.

Why this works: CDN latency wins come almost entirely from network distance, not server processing speed — a perfectly fast origin still can't beat the speed of light for a user on the other side of the planet, which is exactly the problem geographic distribution solves.

Caching user-specific content at a shared CDN edge without per-user cache keys

Wrong

text
-- API returns a personalized dashboard, cached
-- by the CDN keyed only on the URL path
GET /api/dashboard  → cached response

Better

text
-- either mark personalized responses as
-- non-cacheable (Cache-Control: private,
-- no-store), or key the cache on something
-- that varies per user (a Vary header, or a
-- per-user path/query parameter)

What you see: One user briefly sees another user's personalized data — a severe privacy/security bug — because the CDN cached a response keyed only on a URL that was identical across requests from different logged-in users.

Why: A CDN caches by whatever key it is told to use, most commonly the URL — if a personalized response is cacheable by URL alone with no per-user distinction, the CDN cannot tell it apart from a request that legitimately wants the same cached response for everyone, and will serve one user's cached response to the next.

CDN edge cache: hit vs. miss
requestcachedmiss: fetch+ cache

Client

e.g. Tokyo

CDN edge

nearby PoP

Hit: ~5ms

served from edge

Origin

us-east, single region

  • Client — e.g. Tokyo
    • leads to CDN edge (request)
  • CDN edge — nearby PoP
    • leads to Hit: ~5ms (cached)
    • leads to Origin (miss: fetch + cache)
  • Hit: ~5ms — served from edge
  • Origin — us-east, single region

What typically is and isn't a good CDN caching candidate

What typically is and isn't a good CDN caching candidate
ContentGood CDN fitWhy
Images, CSS, JS bundlesYesIdentical for every user, changes rarely, versioned by filename
A public blog post pageYesSame content for every visitor, changes infrequently
A logged-in user's dashboardNo (without care)Different content per user — caching it risks leaking one user's data to another
A public product listing API responseOften yes, with a short TTLSame for all users, tolerates brief staleness

Remember: A CDN caches content at edge locations near users, cutting both network latency and origin load — for static assets by default, and for dynamic content when it is explicitly marked cacheable and does not vary per user.

See also: cache control and invalidation · cacheable vs private content

Cache-Control headers, TTLs, invalidation and signed URLs

coreintermediate

Cache-Control headers are the explicit contract between origin and CDN about what can be cached, for how long, and by whom — max-age sets freshness duration, s-maxage overrides it specifically for shared caches like CDNs, private/public control whether a shared cache may store the response at all. Invalidation purges already-cached content before its TTL expires, and signed URLs restrict access to cached content to authorized requests only.

Think of it as

Cache-Control headers are shipping instructions written on a package: "keep for 3 days" (max-age), "keep at the warehouse for a week but only 3 days once it reaches the customer" (s-maxage), "for this customer's eyes only, do not store at the warehouse" (private). Invalidation is calling the warehouse to recall a package early, before its label's date runs out. A signed URL is a package with a tamper-evident, time-limited shipping label — anyone can see the package sitting at the warehouse, but only a valid label lets it actually be delivered.

http
Cache-Control: public, max-age=300, s-maxage=3600
-- browser caches 5 minutes, CDN caches 1 hour

Cache-Control: private, no-store
-- never cached by any shared cache (CDN, proxy)

What we're doing: Show why a content update needs explicit invalidation, not just a shorter TTL.

cdn-invalidation.txttext
/logo.png cached with Cache-Control: max-age=86400
(24 hours) across dozens of CDN edge locations.

A new logo is deployed at 10:00am. Without explicit
invalidation, every edge location that already cached
the old logo keeps serving it for up to 24 hours from
whenever it was first cached — potentially different
expiry times at different edges.

Fix: issue a CDN purge/invalidation request for
/logo.png immediately after deploy. Edges that
receive the purge drop their cached copy and re-fetch
from the origin on the next request.

Common real-world alternative: version the filename
itself (/logo.v2.png) so the "old" cached object is
simply never requested again — no purge needed.
6
This is the core problem: dozens of edges, each caching independently, mean the update rolls out unevenly and slowly without help.
14
Filename versioning sidesteps invalidation entirely — a very common, simpler alternative for static assets.

Why this works: TTL alone only bounds how stale content can get, it does not make an update propagate immediately — a design that needs an update to take effect right away needs either explicit invalidation or a cache-busting strategy like filename versioning, not just a shorter TTL.

Shortening the TTL as a substitute for invalidation to make deploys "safer"

Wrong

text
Cache-Control: max-age=30
-- set very short "just in case a deploy needs
-- the CDN to pick up changes fast"

Better

text
Cache-Control: max-age=86400
-- long TTL for real caching benefit, PLUS an
-- explicit purge call as part of the deploy
-- pipeline for content that actually changed

What you see: The CDN's cache hit rate is far lower than the content's actual change frequency would justify, and origin load stays high despite having a CDN — because a very short TTL was chosen defensively instead of using invalidation for the (much rarer) actual update event.

Why: A short TTL "just in case" pays the caching benefit away on every single request in exchange for a freshness guarantee that explicit invalidation could provide only on the rare occasions content actually changes — invalidation targets the real event, a short TTL punishes every request regardless of whether anything changed.

Deploying a new logo without waiting out the TTL
Origin
CDN Edge
Browser
  1. 1. logo.png, max-age=86400
  2. 2. serves cached logo (old)
  3. 3. purge /logo.pngdeploy triggers invalidation
  4. 4. re-fetch on next request
  5. 5. serves fresh logo
  1. Origin → CDN Edge: logo.png, max-age=86400
  2. CDN Edge → Browser: serves cached logo (old)
  3. Origin → CDN Edge: purge /logo.png (deploy triggers invalidation)
  4. CDN Edge → Origin: re-fetch on next request
  5. CDN Edge → Browser: serves fresh logo

Common Cache-Control directives for CDN use

Common Cache-Control directives for CDN use
DirectiveEffectTypical use
max-age=3600Fresh for 1 hour, browser + CDNContent that changes hourly at most
s-maxage=3600CDN-specific TTL, overrides max-age for shared cachesDifferent freshness policy for CDN vs browser
privateOnly the browser may cache; CDN must notPer-user personalized responses
no-storeNever cached anywhereSensitive data that must always hit the origin
must-revalidateOnce stale, must check with origin before reuseContent where serving stale after expiry is unacceptable

Remember: max-age sets freshness, s-maxage overrides it for shared caches, private/no-store keep content out of shared caches. TTL alone bounds staleness — an actual content change needs explicit invalidation or filename versioning to propagate immediately. Signed URLs restrict cached content to authorized requests.

See also: cdn fundamentals · cacheable vs private content

Advertisement

Deciding what is safe to cache

The test for whether a response is genuinely safe to cache at a shared edge.

Identifying content that is safe to cache vs user-specific content

standardintermediate

Deciding whether a given response is safe to cache at a shared CDN edge comes down to one question: is this response identical for every requester who could plausibly ask for it? If yes, it is generally safe to cache publicly. If the response depends on who is asking — their identity, permissions, or personalized data — caching it at a shared edge without extra care risks serving one person's private response to someone else.

Think of it as

A shared CDN cache is like a photocopier left in a public lobby with its last output tray still full. A public flyer copied there is fine for anyone to grab. A personal letter photocopied there by mistake is a real problem the instant a stranger picks it up instead of the intended recipient — the test is not "was this convenient to copy," it's "would it be fine for literally anyone in the lobby to pick up this copy."

What we're doing: Show a subtle version of the private-content-cached-as-public mistake, beyond the obvious dashboard case.

subtle-private-leak.txttext
GET /api/search?q=laptops
Response varies ONLY by the query string — looks
perfectly safe to cache publicly, keyed on the URL.

But: the response also includes a "recently viewed
by you" section, personalized per logged-in user,
embedded in the same JSON payload.

Caching this response by URL alone means the FIRST
user's "recently viewed" list gets served to every
subsequent user who searches the same query — a
privacy leak hidden inside an otherwise-cacheable
response.

Fix: separate the response into a cacheable public
part (search results) and a private part (recently
viewed), fetched independently — or mark the whole
response private and accept the caching loss.
7
This is the trap — most of the response really is public, but one embedded field is not.
10
The leak happens silently: the cache has no way to know part of the payload was supposed to differ per user.

Why this works: The riskiest cacheability mistakes are not the obvious ones (an entire dashboard marked public) — they are a mostly-public response with one personalized field embedded in it, which passes a quick review but still leaks data once cached.

Assuming an endpoint is safe to cache because most of its response is shared

Wrong

text
"This endpoint is 95% the same for every
user — let's cache the whole response publicly,
it's basically shared content."

Better

text
"Split the response: cache the shared 95% at
the edge, fetch the personalized 5% separately
(a second request, or client-side rendering) —
or, if splitting isn't worth the engineering
cost yet, mark the whole response private and
accept the smaller caching win."

What you see: A privacy incident traced to a single field in an otherwise-generic, heavily-cached API response — the kind of bug that passes casual review because "the endpoint is basically public," right up until the one personalized field it contains gets served to the wrong person.

Why: Cacheability is a property of the entire response, not a percentage — a single personalized field anywhere in a cached payload is enough to leak one user's data to every other user who happens to request the same cache key.

Safe to cache at a shared edge?

Cacheable

  • +Identical response for any requester
  • +Public marketing page, static asset
  • +Product catalog API with no auth

Not cacheable (as-is)

  • Differs by who is asking
  • Account dashboard, personalized feed
  • Even one embedded personalized field taints the whole response
  • Cacheable
    • Identical response for any requester
    • Public marketing page, static asset
    • Product catalog API with no auth
  • Not cacheable (as-is)
    • Differs by who is asking
    • Account dashboard, personalized feed
    • Even one embedded personalized field taints the whole response

Deciding cacheability by content type

Deciding cacheability by content type
ContentCacheable at a shared edge?Reasoning
Public marketing pageYesIdentical for every visitor
Product catalog API (no auth)Yes, with a TTLSame data for all callers, tolerates brief staleness
"Your recent orders" pageNo — or only with per-user cache keysDifferent content depending on who is logged in
Search results for a public query stringOften yes, keyed by the query itselfSame results for anyone running the same search
A permission-gated admin reportNoAccess itself must be checked per request, not assumed from a cached copy

Remember: Cache only what is truly identical for every requester. A response that is "mostly" shared but contains even one personalized field is not safe to cache as a whole — split it or mark the whole thing private.

See also: cache control and invalidation · cdn fundamentals

Advertisement