CDN & Browser Caching

What is it: A CDN (Content Delivery Network) is a geographically distributed network of edge servers that cache content close to users. Browser caching stores responses locally in the browser's cache, serving them without any network request.

Why: Latency from Vietnam to a US origin server = 150–200ms RTT. A CDN edge server in Singapore = 10–20ms RTT. For static assets (images, JS, CSS), serving from a nearby edge reduces latency by 10×. Browser caching eliminates latency entirely for unchanged resources — no network at all.

Cache-Control header (server → client): - max-age=3600: cache this response for 3600 seconds. After expiry, re-fetch. - no-cache: always revalidate with the server before using the cached version (send conditional request). - no-store: never cache — every request fetches fresh (sensitive data). - s-maxage=600: CDN cache duration (overrides max-age for shared caches, not browsers). - private: only the browser may cache, not CDN (user-specific data like profile pages). - public: CDN may cache this response. - immutable: browser should never revalidate during max-age (for content-hashed assets).

sequenceDiagram participant B as Browser participant Edge as CDN Edge participant O as Origin Note over B: within max-age B->>B: serve from local cache (0ms, no network) Note over B,O: after max-age expires, resource UNCHANGED B->>Edge: GET + If-None-Match: "abc123" Edge->>B: 304 Not Modified (headers only, no body) Note over B,O: after max-age expires, resource CHANGED B->>Edge: GET + If-None-Match: "abc123" Edge->>O: forward (edge cache miss too) O->>Edge: 200 + new body + new ETag Edge->>B: 200 + new body

ETag & conditional requests: The server sends ETag: "abc123" (a fingerprint of the resource). On re-fetch after expiry, the browser sends If-None-Match: "abc123". If unchanged, the server returns 304 Not Modified (no body — saves bandwidth). If changed, the server returns 200 with the new body and a new ETag.

Last-Modified / If-Modified-Since: An alternative to ETag using timestamps. Less reliable than ETags (timestamp granularity, clock skew).

Why the second request is faster: - If max-age hasn't expired: served from browser cache with zero network I/O (~0ms). - If expired but resource unchanged: server returns 304 Not Modified — only headers, no body. ~RTT with no bandwidth cost. - If resource changed: full response from CDN edge (nearby, low latency) instead of origin.

CDN cache invalidation: CDN edges respect Cache-Control TTLs. For immediate invalidation, use: versioned URLs — app.v2.js is a new URL, so the old app.v1.js can be cached indefinitely (cache-control: immutable); or purge via CDN API (Cloudflare, CloudFront purge API) to evict specific keys from all edge nodes.

Trade-offs: Long TTLs: fewer origin requests, better performance, but stale content until expiry. Short TTLs: always fresh, more origin requests, higher origin load. The solution: content-hash the filename for static assets (Webpack/Vite does this automatically) — the URL changes only when content changes, so you can use max-age=31536000, immutable safely.

Common pitfall

Setting a long max-age on an HTML file that references content-hashed assets by a non-hashed filename (main.js instead of main.a1b2c3.js) breaks the entire strategy — the HTML itself now has to stay fresh (short or no cache) for its asset references to ever update, while the assets it points to can safely cache forever. Mixing this up — caching the HTML aggressively and the assets conservatively, the reverse of what's needed — is a common way "we already do content-hashed caching" still ships stale pages to users.