Why the HTTP cache matters

Every fetch over the network costs time and, potentially, money. Large responses require multiple roundtrips, pages stay blank until critical resources finish downloading, and users on limited data plans pay for every unnecessary byte. The browser's HTTP cache is the first tool available to avoid those costs. It's not the most advanced caching strategy, and you can't finely control response lifetimes, but it's universally supported, effective, and requires minimal setup.

The basics of cache-controlled responses

There's no single API called the HTTP Cache. It's the umbrella term for a set of web platform standards—Cache-Control, ETag, and Last-Modified—all of which have full support across major browsers.

The cache is not an all-or-nothing system. Before making any network request, browsers check the cache for a usable response. If one exists, it's served locally, eliminating both latency and data transfer. Whether a cached response is usable depends on a combination of request headers (which your application code influences) and response headers (which your web server configuration controls).

Request headers: rely on browser defaults

For most applications, the request side of caching requires no explicit work. Browsers automatically attach freshness-checking headers like If-None-Match and If-Modified-Since based on their internal cache state. This means ordinary markup such as <img src="my-image.png"> gets HTTP caching for free—no JavaScript or custom header logic needed.

Response headers: server configuration is key

The most impactful caching decisions happen on the server. The headers your server returns on each response determine how browsers and intermediate caches behave. Three response headers matter most:

  • Cache-Control instructs caches when and how long to store a given response, including whether intermediate caches are allowed to reuse it.
  • ETag is typically a hash of a file's contents. When a cached copy expires, the browser sends the token back to the server; if it matches, no re-download is necessary.
  • Last-Modified offers similar revalidation but with a time-based approach instead of content-based comparison.

Some servers set these headers by default; others require explicit configuration. The right settings depend on your server. Documentation is available for popular platforms including Express, Apache, nginx, Firebase Hosting, and Netlify.

Notably, omitting Cache-Control does not disable HTTP caching. All modern browsers fall back to heuristic guessing based on the response type—behavior that is rarely what you want. Since those guesses aren't predictable or standardized, baseline caching headers are worth configuring even when a more sophisticated caching layer is planned later.

Header values for two caching scenarios

Configuring response headers comes down to two main scenarios. Knowing which one applies to a given URL tells you which Cache-Control directives to set.

Versioned URLs: trust the cache for a year

Versioned URLs—those embedding a fingerprint or version number in the filename, such as style.x234dff.css—are a good practice because the content at those URLs is immutable. When the underlying content changes, the URL changes too. This sidesteps the fundamental limitation of browser caching: once a response is cached, it stays in use until it expires via max-age or expires, or until the user clears the cache. If you need to push an emergency update to a file cached for a year, you cannot invalidate it without changing its URL.

For responses to versioned URLs, set:

Cache-Control: max-age=31536000

This value (31,536,000 seconds) is the maximum supported lifetime. During that year, the browser can serve the resource directly from the HTTP Cache without any network request to your server. Build tools such as webpack can automate the process of adding hash fingerprints to asset filenames.

Unversioned URLs: revalidate efficiently

Not every URL can be versioned. HTML documents, for example, almost never carry fingerprint information, and you cannot force a build step in every deployment. For these URLs, HTTP caching cannot fully eliminate the network request, but you can still make that request cheap and fast.

The key Cache-Control directives for unversioned resources are:

  • no-cache — the browser must revalidate with the server before every use of a cached copy.
  • no-store — neither the browser nor intermediate caches (like CDNs) may store the response.
  • private — only the browser may cache the response; intermediate caches cannot.
  • public — any cache may store the response.

Directives can also be combined as a comma-separated list. To decide which values fit a given resource, see the Appendix: Cache-Control flowchart and the Appendix: Cache-Control examples.

For revalidation to work well, your responses should include an ETag or Last-Modified header. Both serve the same purpose—determining whether an expired cached file needs to be re-downloaded—but ETag is more accurate. Here is the logic in practice: a cached response expires after 120 seconds, and the browser initiates a new request. Rather than downloading the full resource again, the browser sends the validation token from ETag (typically a hash of the file's contents) in the If-None-Match request header. If the server confirms the token still matches, it responds with 304 Not Modified, telling the browser to keep its existing copy. The same mechanism works with Last-Modified and the If-Modified-Since header. A 304 response carries very little data, so this revalidation is usually far faster than transferring the resource again.

A visualization of a client requesting a resource and the server responding with a 304 header.
The browser requests /file from the server and includes the If-None-Match header to instruct the server to only return the full file if the ETag of the file on the server doesn't match the browser's If-None-Match value. In this case, the 2 values did match, so the server returns a 304 Not Modified response with instructions on how much longer the file should be cached (Cache-Control: max-age=120).

The HTTP Cache reduces unnecessary network requests and works in all browsers with minimal setup. A sensible starting point:

  • Cache-Control: no-cache for resources that must be revalidated before each use.
  • Cache-Control: no-store for resources that must never be cached.
  • Cache-Control: max-age=31536000 for versioned resources.

Add ETag (or Last-Modified) so that expired cached resources can be revalidated cheaply.

Further reading

For details beyond the basics of Cache-Control, see Jake Archibald's Caching best practices and max-age gotchas. The Love your cache guide covers optimizing cache behavior for return visitors.

Additional optimization tips

  • Serve consistent URLs. The same content under different URLs will be fetched and stored multiple times.
  • Split frequently changing code from stable code. If part of a CSS file—say, library code—rarely changes but your own rules update often, put them in separate files. Apply a short caching duration to the frequently updated file and a long one to the stable file.
  • Consider the stale-while-revalidate directive if your policy can tolerate some staleness.

Appendix: Cache-Control flowchart

Flowchart
The decision process for setting your Cache-Control headers.

Appendix: Cache-Control examples

Cache-Control value Explanation
max-age=86400 The response can be cached by browsers and intermediary caches for up to 1 day (60 seconds x 60 minutes x 24 hours).
private, max-age=600 The response can be cached by the browser (but not intermediary caches) for up to 10 minutes (60 seconds x 10 minutes).
public, max-age=31536000 The response can be stored by any cache for 1 year.
no-store The response is not allowed to be cached and must be fetched in full on every request.