Browser Caching Freshness and Revalidation

Overview
Browser caching solves a precise coordination problem: a browser already has a representation of a resource, but it must decide whether reusing that stored representation preserves HTTP semantics closely enough for the current request. Without caching, every navigation, script load, stylesheet load, image load, and API fetch pays network latency and transfers bytes that may be identical to bytes already present on the device. Without validation rules, reuse can return obsolete content after an origin has changed it. Freshness lifetime and revalidation are the HTTP mechanisms that separate safe reuse from unsafe reuse.
The core distinction is between a fresh response and a stale response. A fresh response can be reused without contacting the origin, subject to request method, response status, cache key, and header constraints. A stale response normally requires revalidation: the browser sends a conditional request using a validator such as ETag or Last-Modified, and the origin either returns 304 Not Modified or a new representation. This makes caching a state-management protocol, not only a storage feature. The browser cache stores response metadata, computes freshness from HTTP fields, and performs conditional network exchanges when the stored response has aged beyond its allowed lifetime.
How it works
At the systems level, the browser’s HTTP cache sits below high-level loading algorithms such as navigation, subresource fetching, and the Fetch API. A cache entry is keyed by the request URI plus relevant request properties, and the Vary response header can add selected request header fields to the secondary key. For example, a response with Vary: Accept-Encoding is not interchangeable across all encodings. Modern browsers also apply cache partitioning policies for privacy, so the same URL may not map to a single global cache entry across top-level site contexts, even when the HTTP headers are otherwise identical.
Freshness is computed from response metadata. Cache-Control: max-age=N gives a freshness lifetime in seconds for private browser caches; Expires gives an absolute expiration time but is overridden by max-age when both are present. The response’s current age is derived from the Date header, the Age header if present, and resident time in the cache. If current age is less than freshness lifetime, the cached response can be used without revalidation. If it is greater, the cache treats the response as stale unless a directive or browser reload mode changes the behavior.
Revalidation uses conditional requests defined by HTTP semantics. If the cached response has an ETag, the browser can send If-None-Match with that entity tag. If it has Last-Modified, it can send If-Modified-Since. If both validators exist, If-None-Match is the stronger and more precise mechanism and takes precedence for recipient evaluation. A 304 Not Modified response carries updated metadata, not a new body; the browser combines the retained body with the new headers according to HTTP caching rules. A 200 response replaces the stored representation with the new one if it is cacheable.
The HTTP cache is separate from, but can be affected by, the Fetch API and service worker interception. Request.cache values such as default, no-store, reload, no-cache, force-cache, and only-if-cached are specified by the WHATWG Fetch Standard and influence whether the browser consults, bypasses, or revalidates the HTTP cache. A service worker can implement additional application-defined caching through the Cache API, but that is not the same store as the user agent’s built-in HTTP cache. Correct designs avoid assuming that a Cache API entry and an HTTP cache entry share invalidation behavior.
Cache-Control: max-age=300- No exchanges yet — press Navigate to issue the first request.
Model figures: 138.7 kB body, 320 B of response headers, 120 ms round trip, 360 ms transfer. Decisions follow RFC 9111 freshness and validation, RFC 8246 (immutable) and RFC 5861 (stale-while-revalidate). Reload is modelled as an ordinary browser reload — a conditional request carrying Cache-Control: max-age=0 — not a force reload.
What the standard says
The current HTTP caching specification is RFC 9111, HTTP Caching, which replaced the caching parts of RFC 7234. HTTP semantics, including validators, conditional requests, status codes, and header-field definitions, are in RFC 9110, HTTP Semantics. The Fetch Standard specifies browser fetch integration and request cache modes. RFC 8246 defines the immutable Cache-Control extension. RFC 5861 defines stale-while-revalidate and stale-if-error; these are registered Cache-Control extensions and are not a replacement for RFC 9111 freshness calculation.
RFC 9111 requires a cache not to reuse a stored response unless it can be matched to the presented request and the response is either fresh, allowed to be served stale, or successfully validated. Cache-Control: no-store instructs a cache not to store the response. Cache-Control: no-cache does not mean “do not store”; it means the response must not be used to satisfy a later request without successful validation. must-revalidate requires a cache not to reuse a stale response unless it has been validated by the origin. private restricts storage by shared caches, while public permits storage even when the response would not otherwise be reusable by a shared cache.
The specification defines explicit and heuristic freshness. Explicit freshness comes from directives such as s-maxage, max-age, and Expires, with s-maxage applying to shared caches and overriding max-age there. Heuristic freshness is permitted only when explicit freshness is absent and the status code is cacheable by default or explicitly marked cacheable. RFC 9111 documents a specific heuristic limit: a cache should not use a heuristic freshness lifetime greater than 10 percent of the time since Last-Modified. That is a recommendation, not a browser-wide guarantee, and competent implementations should prefer explicit Cache-Control.
Validation semantics are equally specific. RFC 9110 defines ETag as an entity tag that can be strong or weak; weak tags use the W/ prefix and are suitable for semantic equivalence but not for every range-update use case. Last-Modified has one-second timestamp granularity and can be ambiguous for resources changed more than once within a second. A 304 Not Modified response is only meaningful in response to a conditional request and must not include a message body. Header fields received on the 304 update the stored response metadata, which can extend or change the next freshness lifetime.
Trade-offs
The main measurable benefit is avoided work. A fresh cache hit avoids at least one network round trip and usually avoids response-body transfer. A revalidated hit still pays request latency but can avoid body bytes because 304 carries metadata only. In browser instrumentation, the Resource Timing API exposes transferSize, encodedBodySize, decodedBodySize, responseStart, and responseEnd on PerformanceResourceTiming. These fields allow a system to distinguish full transfers, revalidations, and some local cache outcomes, subject to Timing-Allow-Origin restrictions for cross-origin resources.
The main correctness cost is bounded staleness. If a stylesheet has Cache-Control: max-age=3600, a conforming cache can reuse it for up to 3600 seconds after the response becomes resident, adjusted by age calculation, without asking the origin. That is useful for stable, fingerprinted files but risky for mutable URLs. Conversely, Cache-Control: no-cache reduces staleness but adds validation traffic on repeat use. no-store minimizes local persistence but removes most browser-cache reuse. These are not equivalent privacy, correctness, or performance settings; they change different parts of the caching state machine.
Cacheability also shifts load across the system. Long explicit freshness reduces origin request count for repeat views and back-forward navigations, but it makes emergency rollback of non-fingerprinted URLs difficult. Conditional caching reduces transferred bytes but can still concentrate round trips at navigation time. Vary improves representation correctness but increases key cardinality; Vary: User-Agent, for example, can fragment the cache far more than Vary: Accept-Encoding. stale-while-revalidate can hide latency by serving a stale response while background validation occurs, but RFC 5861 makes the staleness window explicit, so it should only be used where that additional stale period is acceptable.
Failure modes
A common failure mode is treating no-cache as no-store. no-cache permits storage and requires validation before reuse; sensitive responses that must not be written to persistent cache need Cache-Control: no-store. Another common failure is combining long max-age with mutable filenames. If /app.js changes in place while clients hold Cache-Control: public, max-age=31536000, immutable, those clients can correctly keep using the old bytes for a long time. The minimal safe pattern is content-addressed filenames for long-lived assets and short or validating headers for stable document URLs.
Validator failures are subtler. An origin that generates a new ETag on every response prevents 304 reuse, even when the body is unchanged. An origin that reuses the same strong ETag for different byte sequences violates the validator’s purpose and can cause stale bodies. Last-Modified alone can miss rapid consecutive updates because of one-second resolution. Incorrect Vary handling can return a representation compressed, localized, or negotiated for a different request context. Missing Date headers, clock skew, and intermediary Age headers can also affect freshness computation.
Diagnosis starts by separating fresh hits, conditional hits, and misses. Browser developer tools usually expose whether a response came from memory cache, disk cache, or a 304 exchange, and they show request and response headers. The decisive fields are Cache-Control, Expires, Age, Date, ETag, Last-Modified, Vary, and the conditional request fields If-None-Match and If-Modified-Since. Command-line reproduction can use a first HEAD or GET to capture ETag and Last-Modified, then a second request with the corresponding conditional header to verify whether the origin returns 304 or a full 200 representation.
Some failures are caused by layering. A service worker may return a response from the Cache API while the network panel appears to show an HTTP-cache pattern. A reload may set request cache behavior that bypasses normal reuse. Cross-origin subresources may hide timing details unless Timing-Allow-Origin permits disclosure. Shared intermediaries obey rules that differ from a private browser cache, especially around s-maxage, private, public, and responses to authorized requests. Diagnosis is therefore most reliable when the request path, cache layer, and exact request cache mode are identified before interpreting headers.
A minimal implementation
A correct minimal implementation uses two classes of URLs. Mutable documents use validation, for example: Cache-Control: no-cache with either ETag or Last-Modified. This allows storage but forces the browser to revalidate before reuse, so deployments can update the document without waiting for a long freshness lifetime to expire. Static assets use content-addressed URLs, such as filenames containing a cryptographic digest of the bytes, and can then use Cache-Control: public, max-age=31536000, immutable. RFC 8246 defines immutable as a signal that the representation will not change during its freshness lifetime.
The origin must generate validators consistently. ETag values should change when the selected representation changes and remain stable when it does not. If content negotiation is used, the validator must correspond to the selected representation, and Vary must list the request header fields that affect selection. Last-Modified should reflect the representation’s modification time, but it should not be the only validator for resources that can change more than once per second. A 304 response should include updated cache metadata such as Cache-Control, Expires, ETag, Last-Modified, and Vary when applicable.
A minimal header set is therefore: for HTML or other mutable entry points, Cache-Control: no-cache and ETag: "stable-version-token"; for fingerprinted assets, Cache-Control: public, max-age=31536000, immutable and an optional ETag; for responses that must not be stored, Cache-Control: no-store. This is small, but it covers the main state transitions: fresh reuse for immutable assets, conditional reuse for mutable documents, and no persistence for responses that require it. The implementation is correct only if URL naming, validators, and Vary are treated as part of the same cache contract.