HTTP Caching

Store responses locally to reduce latency and server load

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.