"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:
- Browser cache — per-user, local disk + memory. Hit ratio: highest (same user returns to same page). Bandwidth saved: outright avoided.
- 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.
- 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.
- 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 headers —
CDN-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 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
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.