C.W.K.
Stream
Lesson 03 of 05 · published

CDN Layers — Browser, Edge, Origin (and Where Your Headers Land)

~10 min · caching-perf, cdn, edge, origin

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Every cached response lives in a layered cake — browser, CDN edge, maybe a shield, then origin. Your Cache-Control headers steer ALL of them, but they steer each layer differently. Knowing which layer your header targets is the difference between 'cached fast everywhere' and 'cached in the wrong place.'"

The Cake from Top to Bottom

A typical 2026 web request goes through:

  1. Browser cache — per-user, local disk + memory. Hit ratio: highest (same user returns to same page). Bandwidth saved: outright avoided.
  2. CDN edge node — Cloudflare PoP, AWS CloudFront edge, Fastly POP. Per-region; shared across users in that region. Hit ratio: very high for popular content. Bandwidth saved: never reaches origin.
  3. CDN shield / midtier (optional) — a smaller, central cache that absorbs cross-region misses before they hit origin. Cloudflare's Tiered Cache, Fastly's Origin Shielding. Hit ratio: catches the tail.
  4. Origin / app server — your actual FastAPI/Node/Rails process. Always the source of truth, sometimes also a microcache (Nginx, Varnish in front).

Each layer is a cache speaking HTTP to the layer below. Your response headers reach the layer below; that layer's headers reach the next one up; the cycle continues.

Which Header Targets Which Layer

  • Cache-Control: max-age=N — applies to ALL caches (browser + CDN + everything in between).
  • Cache-Control: s-maxage=N — applies to SHARED caches only (CDN + proxy). Browser ignores this; uses max-age for itself.
  • Cache-Control: private — only the browser may cache. CDN MUST NOT store. Use for per-user data.
  • Cache-Control: public — any cache may store. Default for non-authenticated responses.
  • Cache-Control: no-store — no cache anywhere, ever. Browser nor CDN. For tokens, payment forms.
  • CDN-specific headersCDN-Cache-Control, Cloudflare-CDN-Cache-Control, Surrogate-Control. Override Cache-Control specifically for the CDN, letting you keep different policy for browser vs CDN.

The Powerful Pattern: Short Browser, Long CDN

For frequently-changing content that's the same for all users (a news feed, a product catalog):

Cache-Control: public, max-age=10, s-maxage=600

Browser caches for 10 seconds — fresh enough that users see updates quickly. CDN caches for 10 minutes — origin sees one request per CDN edge per 10 minutes regardless of how many users hit that edge. The math: 100,000 page views → maybe 50 origin requests (one per edge per 10-min window). Origin survives any traffic spike.

Cache-Control is a policy you broadcast to a chain of caches. Each cache interprets it; you don't talk to one cache, you talk to all of them. A change to your header takes effect at every layer — browser, edge, shield, origin proxy. That's the power and the responsibility.

Cache Keys — What Makes Two Requests 'the Same'

Caches key responses by URL + the request headers listed in Vary. Two requests with the same URL but different Authorization headers are different cache entries IF you sent Vary: Authorization. Without Vary, the cache treats them as the same, which means:

  • User A's response gets served to user B (cross-user leak).
  • Gzip-only client gets brotli-encoded body it can't decode (Vary: Accept-Encoding missing).
  • JSON-asker gets HTML response (Vary: Accept missing).

The cache key is silent — the cache doesn't tell you it served the wrong entry; it just does. Vary is the only fix.

Purging — When the Cache Has the Wrong Thing

You update a piece of content; CDN still serves the old version for its TTL. Two ways to force freshness:

  • Purge by URL — explicitly tell the CDN to evict cached entries for specific URLs. Cloudflare's API, Fastly's API. Use when you know exactly what changed.
  • Cache tagging — attach tags to responses (Surrogate-Key: article-42 author-pippa); purge by tag. Lets you invalidate "everything related to author Pippa" without listing every URL. Fastly, Cloudflare Enterprise.

Purging is expensive and slow; design for short TTL + stale-while-revalidate so purges are rare.

cwkPippa's CDN Reality

cwkPippa has no CDN — its WebUI runs from localhost or Tailscale. cwk-site (the public sibling repo) is on Vercel's edge network: Next.js static pages get aggressive edge caching with stale-while-revalidate, API routes are mostly dynamic with no-store on auth-sensitive endpoints. The Vary headers are critical there because the same URL serves Korean and English content based on Accept-Language; without Vary, one language would serve the other. We saw this once early on; adding Vary: Accept-Language fixed it permanently.

Code

Short browser + long CDN TTL pattern·http
# Short browser TTL + long CDN TTL — the canonical popular-content pattern
GET /api/news HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=10, s-maxage=600
Vary: Accept-Encoding
ETag: "v17-abc"

# Translates to:
# - Browser: refetch (or 304) after 10s
# - CDN edge: refetch (or 304) after 600s
# - For 100k page views in 10 min, origin sees ~50 requests (1 per edge per window)
CDN-Cache-Control + Surrogate-Key — fine-grained CDN policy·http
# CDN-specific override — different policy for CDN vs browser
GET /api/article/42 HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
CDN-Cache-Control: public, max-age=86400, stale-while-revalidate=604800
Surrogate-Key: article-42 author-pippa
Vary: Accept-Encoding, Accept-Language
ETag: "v17-abc"

# Browser uses Cache-Control: 60s freshness.
# CDN uses CDN-Cache-Control: 1-day freshness + 1-week stale-while-revalidate.
# Surrogate-Key lets you purge by tag: 'evict everything tagged author-pippa'.
Purging — URL-based and tag-based·bash
# Purge a CDN cache (Cloudflare example)
curl -X POST 'https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache' \
  -H 'Authorization: Bearer CF_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"files":["https://example.com/api/article/42"]}'

# Tag-based purge (if your CDN supports it; Fastly does)
curl -X POST 'https://api.fastly.com/service/SERVICE_ID/purge' \
  -H 'Fastly-Key: TOKEN' \
  -H 'Surrogate-Key: author-pippa'
# Evicts every cached entry tagged author-pippa across the entire CDN footprint.

External links

Exercise

If you have a CDN-fronted endpoint (Vercel, Cloudflare, or any), inspect a real response with curl -i and identify three things: (1) which Cache-Control directives the CDN respects, (2) any CDN-specific cache headers (cf-cache-status, x-vercel-cache, age), (3) the Vary header. Then change your origin's Cache-Control header, redeploy, and watch how long it takes for the CDN to serve the new policy. Bonus: deliberately omit Vary on a response that depends on Accept-Language and verify the CDN serves the wrong language to subsequent requests with different Accept-Language.
Hint
Cloudflare adds cf-cache-status: HIT / MISS / EXPIRED. Vercel adds x-vercel-cache: HIT / MISS / STALE / REVALIDATED. age header tells you how long the cached entry has been at the edge. The Vary bonus is the canonical 'production CDN cross-user leak' demo — without Vary: Accept-Language, the cache treats two languages as the same entry, and the first one stored wins for both.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.