Why Caching Matters
Caching stores copies of responses so they can be reused without contacting the server again. Done right, caching dramatically reduces latency and server load. Done wrong, users see stale data.
Where Caching Happens
- Browser cache: Your browser caches responses locally. Fastest, closest to the user.
- CDN / Reverse proxy: Services like Cloudflare, Nginx, or Varnish cache responses at the edge, close to users worldwide.
- Shared proxy: Corporate or ISP proxies that cache responses for multiple users.
Cache-Control Directives
The Cache-Control header is the primary way to control caching behavior:
| Directive | Meaning |
|---|---|
public |
Any cache (browser, CDN, proxy) can store this response. |
private |
Only the browser can cache it — not CDNs or shared proxies (e.g., user-specific data). |
no-cache |
Cache can store the response, but must revalidate with the server before using it. |
no-store |
Do not cache at all. Every request goes to the server. Use for sensitive data. |
max-age=N |
Response is fresh for N seconds. After that, the cache must revalidate. |
s-maxage=N |
Like max-age but only for shared caches (CDNs, proxies). Overrides max-age for those caches. |
must-revalidate |
Once stale, the cache must not serve the response without revalidating (no serving stale content while offline). |
immutable |
The response will never change (used for versioned assets like app.v3.js). |
Validation: ETag and Last-Modified
When a cached response expires, the browser doesn't re-download the whole thing. Instead, it asks the server: "has this changed?" This is called a conditional request or validation:
| Mechanism | Server Sends | Browser Sends (on revalidation) |
|---|---|---|
| ETag | ETag: "abc123" |
If-None-Match: "abc123" |
| Last-Modified | Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT |
If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT |
If the resource hasn't changed, the server responds with 304 Not Modified (no body) — saving bandwidth.
The 304 Not Modified Flow
Browser Server │ │ │ 1. First request │ │ GET /style.css ───────────────────────────▶ │ │ │ │ HTTP/1.1 200 OK │ │ ETag: "v42" │ │ Cache-Control: max-age=3600 │ │ [full CSS file] ◀───────────────────────── │ │ │ │ 2. After max-age expires (revalidation) │ │ GET /style.css │ │ If-None-Match: "v42" ─────────────────────▶│ │ │ │ (Server checks: ETag still "v42"? Yes!) │ │ │ │ HTTP/1.1 304 Not Modified │ │ (no body — use cached version) ◀──────────│ │ │
The 304 response is tiny (just headers, no body), so the bandwidth savings are significant for large resources like stylesheets, scripts, and images.