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.publicvs.private: Apublicdirective allows intermediate caches (such as CDNs) to store the response, making it available to multiple users. Aprivatedirective 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 Modifiedresponse with an empty body, instructing the browser to reuse its local copy. - If changed: The server returns an
HTTP 200 OKresponse along with the updated asset and a newETag.
How Cache-Control and ETag Interact
Cache-Control and ETag solve two distinct
phases of the caching lifecycle: determination of freshness and
conditional revalidation.
- Initial Request: The client requests
main.js. The server returns the file withCache-Control: max-age=3600, no-cacheandETag: "abc123xyz". - Subsequent Request: Because
no-cacheis present, the browser cannot use the file blindly. It checks its cache, finds theETag, and sends a request withIf-None-Match: "abc123xyz". - Server Evaluation: If
main.jshasn't changed, the server sends back a tiny304 Not Modifiedheader payload. The browser renders the page using the local file, avoiding a multi-kilobyte or multi-megabyte download.
Recommended Caching Strategy
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.