Making the Web Fast Without Touching the Server

Caching is the single most effective performance technique on the web. A cached response never reaches your origin server, never touches your database, and travels a fraction of the distance. Understanding HTTP caching is essential for anyone building, operating, or debugging web systems—and it is the hidden machinery behind CDNs.

Freshness vs. Validation

Caching has two distinct mechanisms:

  1. Freshness: The response can be reused without contacting the server at all, until it expires. Controlled by Cache-Control: max-age=N and Expires.
  2. Validation: The cached copy is stale, so the client asks the server "has this changed?" using a validator, and the server replies either 304 Not Modified (reuse it) or 200 with a fresh copy.

The Cache-Control Header

Cache-Control is the modern, expressive caching directive. Common values:

  • no-store: never cache this response, anywhere.
  • no-cache: cache it, but revalidate with the server on every use.
  • private: only the browser may cache it (not shared proxies/CDNs).
  • public: any cache may store it.
  • max-age=N: fresh for N seconds.
  • s-maxage=N: fresh for N seconds in shared caches (CDNs) only.
  • stale-while-revalidate=N: serve the stale copy for up to N seconds while refreshing in the background.
  • immutable: never revalidate this response during its freshness lifetime—perfect for hashed asset filenames.

Validators: ETag and Last-Modified

A validator is a fingerprint of a resource: - ETag: an opaque string (often a hash) uniquely identifying this version. The client sends it back as If-None-Match. - Last-Modified: a timestamp. The client sends it back as If-Modified-Since.

If the validator still matches, the server returns 304 Not Modified with no body—saving bandwidth but still costing a round trip. ETag is more precise than Last-Modified because it can detect changes within the same second.

The Vary Header

Vary tells caches which request headers influence the response. Vary: Accept-Encoding means a gzip response must not be served to a client that cannot decompress it. Vary: Accept-Language means localized pages must be keyed separately. Forgetting Vary is a classic source of "Why is my CDN serving the wrong language?" bugs. Vary: * should almost never be used—it makes a response effectively uncacheable.

Cache Busting

You cannot instantly invalidate a shared CDN cache reliably. The robust pattern is cache busting: embed a content hash in the filename (app.4f2a1c.css). Because the URL changes whenever the content changes, caches never serve a stale asset, and you can set Cache-Control: public, max-age=31536000, immutable on those files. HTML entry points get a short TTL so they always reference the latest hashed assets.

Client Caches vs. Shared Caches

Browser caches and intermediate caches (CDNs, corporate proxies) obey the same headers but behave differently. Anything user-specific—session pages, personalized dashboards—must be marked private or no-store, or a shared cache can leak one user's data to another. This is one of the most serious classes of caching vulnerability.

Debugging Caching Problems

Caching bugs are notoriously confusing because different layers serve different versions. The practical method is to identify which cache is responsible:

  • Hard reload bypasses the browser cache but not the CDN. If a hard reload fixes it, the browser cache was stale.
  • A cache-busting query string (?v=2) bypasses every cache keyed on the URL. If that fixes it, an intermediate cache was stale.
  • curl -I shows the response headers directly; look for Age, CF-Cache-Status, X-Cache, or Via to see whether a proxy served it.
  • Missing Vary manifests as one user seeing another's language or encoding. Add the correct Vary and purge.

The golden rule: never rely on manual purges as your deployment strategy. Version your asset URLs, and caching stops being a source of fear.