The Pieces That Make Up Browser Caching

Browser caching is one of the highest-leverage performance tools available to a web developer. Getting it right means returning visitors load your site in milliseconds. Getting it wrong means stale pages, broken deployments, or cache misses on every request. Understanding the pieces that control caching puts you in the driver’s seat.

The Two Layers

Caching happens in two places: the browser’s own cache (private) and intermediate caches like CDNs or reverse proxies (shared). HTTP headers let you control both independently.

Cache-Control

Cache-Control is the primary directive for caching behavior. It replaces the older Expires header and gives you precise control.

max-age: How long, in seconds, a response can be served from cache without revalidating.

Cache-Control: max-age=86400

This tells any cache—browser or CDN—to serve the response for up to one day without asking the server.

s-maxage: Like max-age, but only applies to shared caches (CDNs). You can give browsers a shorter freshness window while keeping CDN caches warm longer.

Cache-Control: max-age=60, s-maxage=3600

no-cache: Counterintuitively, this doesn’t mean “don’t cache.” It means “cache the response, but always revalidate with the server before serving it.” The server can respond with 304 Not Modified and the browser serves from cache—saving bandwidth while ensuring freshness.

no-store: This truly disables caching. Use it for sensitive pages like bank statements or admin dashboards.

immutable: Tells caches that the response will never change during its max-age window. Useful for versioned static assets—browsers skip the revalidation request entirely, even when the user refreshes.

Cache-Control: max-age=31536000, immutable

ETags

An ETag is a fingerprint of the response content generated by the server—usually a hash. When a cached response expires or no-cache is set, the browser sends the ETag back in an If-None-Match header. If the content hasn’t changed, the server responds with 304 Not Modified and no body, saving bandwidth.

# Server sends:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d"

# Browser revalidates with:
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d"

Last-Modified

Similar to ETags, Last-Modified is a timestamp-based alternative. The browser revalidates using If-Modified-Since. ETags are preferred because they’re more reliable—a file can be regenerated with identical content but a new timestamp, which would incorrectly invalidate a Last-Modified cache.

The Vary Header

Vary tells caches that the response may differ based on specific request headers. A common use case is content negotiation:

Vary: Accept-Encoding

This tells caches to store separate copies for gzip-compressed and uncompressed responses. Without it, a CDN might serve the gzip version to a client that doesn’t support it. Vary: Accept-Language enables per-language caching.

Practical Strategy for Static Assets

For static assets with versioned filenames (e.g., main.a3f9c2.js), use aggressive caching:

Cache-Control: public, max-age=31536000, immutable

The filename changes on each deploy, so users always get fresh code, and the old files are cached forever on repeat visits.

For HTML files, use no-cache so browsers always revalidate, but serve from cache on a 304:

Cache-Control: no-cache

This gives you fast loads for unchanged pages while guaranteeing users see new deployments immediately.

Debugging Caches

In Chrome DevTools, the Network tab shows response headers and whether a resource was served from cache ((from disk cache), (from memory cache), or 304). The Cache-Control response header is always your source of truth for what a browser should do with a response.

Understanding these pieces—Cache-Control directives, ETags, Last-Modified, and Vary—gives you the vocabulary to reason about and optimize every resource your site delivers.