How Cache-Control and ETag Work in Web Development

Modern web performance relies heavily on browser caching to reduce server load, save network bandwidth, and deliver fast load times to users. This article explains how two fundamental HTTP headers—Cache-Control and ETag—operate together to manage asset freshness and validation. By mastering these headers, developers can dictate precisely when browsers should store resources locally, when they must re-verify them with the origin server, and how to eliminate redundant data transfers entirely.

The Role of Cache-Control

The Cache-Control HTTP header is the primary mechanism used to define caching policies for both browsers (private caches) and intermediate systems like CDNs or proxies (public caches). It uses directives to control who can cache an asset, for how long, and under what conditions.

Key directives include:

  • max-age=<seconds>: Specifies the maximum amount of time in seconds an asset is considered "fresh." During this window, the browser serves the file directly from its local disk or memory cache without contacting the server.
  • no-cache: Forces the browser to send a validation request to the origin server before using the cached copy. The browser still stores the asset, but it cannot use it without server approval.
  • no-store: Instructs the browser and intermediate caches never to store any version of the request or response. This is typically used for sensitive data such as authentication tokens or banking information.
  • public vs. private: A public directive allows intermediate caches (such as CDNs) to store the response, making it available to multiple users. A private directive restricts caching solely to the end-user's browser.
  • immutable: Signals to the browser that the response body will never change over its lifetime, preventing validation requests even when the user refreshes the page.

The Role of ETag

While Cache-Control dictates expiration time, the ETag (Entity Tag) header handles validation. An ETag is an opaque identifier, usually a hash of the file contents or a version string, assigned by the web server to a specific version of a resource.

When a cached asset's max-age expires or when configured with no-cache, the browser needs to check if the file has changed. Instead of re-downloading the entire asset, the browser sends a conditional request containing the If-None-Match header along with the saved ETag value.

The server then compares the incoming token against the current version of the resource:

  • If identical: The server returns an HTTP 304 Not Modified response with an empty body, instructing the browser to reuse its local copy.
  • If changed: The server returns an HTTP 200 OK response along with the updated asset and a new ETag.

How Cache-Control and ETag Interact

Cache-Control and ETag solve two distinct phases of the caching lifecycle: determination of freshness and conditional revalidation.

  1. Initial Request: The client requests main.js. The server returns the file with Cache-Control: max-age=3600, no-cache and ETag: "abc123xyz".
  2. Subsequent Request: Because no-cache is present, the browser cannot use the file blindly. It checks its cache, finds the ETag, and sends a request with If-None-Match: "abc123xyz".
  3. Server Evaluation: If main.js hasn't changed, the server sends back a tiny 304 Not Modified header payload. The browser renders the page using the local file, avoiding a multi-kilobyte or multi-megabyte download.

Modern web development pairs these headers with content-hashed assets (e.g., bundle.8a7b6c.js).

For versioned static assets (JavaScript, CSS, images), set aggressive caching:

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

Because the filename changes whenever the code changes, these files never need revalidation.

For unversioned files that change frequently (such as index.html), rely on validation:

Cache-Control: no-cache
ETag: "dynamic-hash-value"

This guarantees that clients always check for updates to the entry file before serving it, while still benefiting from 304 responses when no deployment has occurred.