Cache Validation & Conditional Requests
Freshness lifetimes eventually expire, but an expired cache entry is not necessarily wrong — it is only unverified. Validation is the mechanism that lets a cache confirm a stored representation is still correct without re-downloading it. This reference brings together everything on this site about that mechanism: it covers how validators are generated and compared, how conditional requests and 304 Not Modified responses work end to end, and how stale-while-revalidate lets a cache serve immediately while validation happens in the background. The two validator types are covered in depth in ETag generation and validation strategies and in Last-Modified and date-based validation, which together form the complete picture of how a cache decides whether a stored response can still be trusted.
Validation and freshness are two different questions, and conflating them is the single most common source of caching bugs. Freshness asks “has the TTL expired?” — a purely time-based calculation covered in freshness vs validation models explained. Validation asks “is the stored body still an accurate representation of the resource?” — a question that can only be answered by asking the origin, using a validator the origin supplied earlier. A response can be stale yet still valid; validation is what lets a cache serve that same body again without paying the cost of a full retransmission.
RFC and Specification Anchor
Validator and conditional-request semantics span two RFCs, each covering a distinct layer:
- RFC 9110 — HTTP Semantics, Section 8.8 (Validators) defines
ETagandLast-Modifiedas response header fields, distinguishes strong from weak validators, and specifies the comparison functions (strong comparison and weak comparison) used to test equivalence. - RFC 9110 — HTTP Semantics, Section 13 (Conditional Requests) defines the request header fields
If-None-Match,If-Modified-Since,If-Match, andIf-Unmodified-Since, the precedence rules between them, and the304 Not Modifiedstatus code. - RFC 9111 — HTTP Caching, Section 4.3 (Validation) defines how a cache constructs a conditional request from a stored response’s validators when it needs to revalidate, and how it must handle the resulting
304or200response. - RFC 9111 — HTTP Caching, Section 5.2.2 (Stale Directives) defines
stale-while-revalidateandstale-if-error, the directives that let a cache serve a stale stored response instead of blocking on synchronous revalidation.
RFC 9110 §13.1.2 establishes a strict precedence order when a request could plausibly carry more than one conditional header, and RFC 9111 §4.3.1 tells a cache exactly which validator to send when it revalidates a stored response:
Conditional header precedence (evaluated by the origin, per RFC 9110 §13.1):
| Priority | Request header | Rule |
|---|---|---|
| 1 | If-Match |
Evaluated first; used for safe update semantics (PUT/PATCH), not cache revalidation |
| 2 | If-Unmodified-Since |
Ignored if If-Match is present |
| 3 | If-None-Match |
For GET/HEAD revalidation, this is authoritative when the resource has an ETag |
| 4 | If-Modified-Since |
Ignored whenever If-None-Match is present in the same request |
Validator selection (evaluated by the cache, per RFC 9111 §4.3.1):
| Priority | Stored validator | Cache behavior on revalidation |
|---|---|---|
| 1 | ETag present |
Send If-None-Match with the stored ETag value; this alone is sufficient |
| 2 | ETag absent, Last-Modified present |
Send If-Modified-Since with the stored Last-Modified date |
| 3 | Neither present | No conditional request is possible; the cache must either serve stale under an applicable directive or issue a full unconditional request |
The practical consequence of these two tables: an ETag always wins when both validators are available on either side of the exchange, and a cache that stores both should still only send If-None-Match — sending both conditional headers is not required and gives no additional guarantee once If-None-Match is present.
Concept Map & Terminology
Strong validator: A validator guaranteed to change whenever the response body changes in any way, byte-for-byte. RFC 9110 §8.8.1 requires that two responses sharing a strong validator be identical at the octet level. A strong ETag is required for byte-range requests to be combined safely across responses. See ETag generation and validation strategies for the full comparison rules.
Weak validator: A validator that only guarantees semantic equivalence — the representation is close enough to be treated as the same for cache purposes, even if some bytes differ (whitespace, header order in an embedded timestamp, and so on). Weak ETag values are prefixed with W/. Last-Modified is always weak, because its one-second granularity cannot distinguish sub-second edits.
ETag: A response header carrying an opaque token that identifies a specific version of a representation. The origin generates it (commonly from a content hash, a database row version, or a build fingerprint) and the cache stores it alongside the body for future revalidation. Covered fully in ETag generation and validation strategies.
Last-Modified: A response header carrying the timestamp of the resource’s last change, at one-second resolution. It is the fallback validator when an origin cannot cheaply compute a content hash. See Last-Modified and date-based validation for its date-comparison semantics and clock-skew pitfalls.
Conditional request: Any request carrying If-None-Match, If-Modified-Since, If-Match, or If-Unmodified-Since. For cache revalidation specifically, it is a GET or HEAD request that lets the origin respond with 304 Not Modified instead of retransmitting a body the client already holds. Full mechanics are in conditional requests and 304 responses.
304 Not Modified: A status code with no body, sent when the origin confirms the client’s cached representation is still current. RFC 9110 §15.4.5 requires the response to include any headers that would have changed (a refreshed Cache-Control, a new Date) even though the body is omitted. See how to debug 304 Not Modified responses for a worked troubleshooting walkthrough.
Revalidation: The act of a cache sending a conditional request to confirm a stale stored response is still valid, rather than discarding it and fetching a fresh copy unconditionally. A successful revalidation resets the freshness lifetime without any bandwidth cost for the body.
stale-while-revalidate: A Cache-Control directive that permits a cache to serve a stale response immediately while it triggers an asynchronous revalidation request in the background, hiding the latency of validation from the requesting client entirely. See serving stale content with stale-while-revalidate.
stale-if-error: A companion Cache-Control directive that permits a cache to keep serving a stale response when the background revalidation attempt fails with a server error, rather than surfacing the failure to the client. It converts a transient origin outage into a slightly-stale-but-available response instead of a hard error.
Architecture Overview
The diagram below traces one resource through two full request cycles: the initial unconditional fetch that establishes a validator, and a later conditional revalidation that reuses the stored body via a 304.
Two things are worth reading directly off the diagram. First, the body only crosses the network once — in step 3. Every later revalidation reuses that same stored body, and the origin only ever needs to confirm or deny that it is still current. Second, the cache’s job at step 5 is a decision, not a fetch: it must recognize that the stored response is stale and choose revalidation over either blind reuse or a full unconditional refetch.
Cross-Cutting Production Patterns
Pattern 1: Dual validators for maximum compatibility
Origins should send both an ETag and a Last-Modified header whenever both are cheap to compute. This gives clients that only implement date-based conditionals a working fallback while still letting ETag-aware clients get the stronger guarantee.
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
ETag: "v4-9f2c1a"
Last-Modified: Wed, 01 Jul 2026 14:32:07 GMT
On the next request, a cache holding both values sends only If-None-Match — RFC 9110 §13.1.2 says the origin must ignore If-Modified-Since once If-None-Match is present, so sending both conditional headers together adds no value but costs nothing either.
Pattern 2: Conditional revalidation request
The stored validator becomes a request header on the next fetch, once the cached copy has gone stale:
GET /api/products/42 HTTP/1.1
Host: example.com
If-None-Match: "v4-9f2c1a"
If the resource is unchanged, the origin returns a bodyless 304; the round trip carries only headers, no matter how large the underlying representation is.
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=300
Date: Mon, 06 Jul 2026 09:15:44 GMT
Pattern 3: Instant staleness tolerance with background revalidation
Pairing max-age with stale-while-revalidate removes the latency spike that normally occurs the instant a TTL expires — the first request after expiry gets an immediate response while the conditional check happens off the critical path.
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: "v7-4b1e0d"
For the ten minutes following expiry, the cache serves the stored body immediately on every request and fires at most one background If-None-Match check, rather than blocking every concurrent request on a synchronous round trip to origin.
Pattern 4: Tolerating origin failure during revalidation
stale-if-error extends the same idea to failure handling: if the background revalidation request itself fails, the cache falls back to the stale body instead of surfacing an error.
Cache-Control: public, max-age=60, stale-while-revalidate=600, stale-if-error=86400
A transient 5xx or timeout from origin during revalidation is invisible to the client for up to 24 hours — the cache simply keeps serving the last known-good body while it keeps retrying validation in the background.
Diagnostic and Debugging Reference
Confirm a validator is present at all
curl -sI https://example.com/resource | grep -iE 'etag|last-modified|cache-control'
No ETag and no Last-Modified in the response means the origin cannot support conditional revalidation for this resource at all — every request will be a full, unconditional fetch regardless of any Cache-Control directive.
Send a manual conditional request
etag=$(curl -sI https://example.com/resource | grep -i '^etag:' | cut -d' ' -f2- | tr -d '\r')
curl -sI -H "If-None-Match: ${etag}" https://example.com/resource
A 304 Not Modified confirms the origin correctly compares the submitted ETag against the current representation. A 200 with an unchanged body suggests the origin is not implementing conditional comparison at all — it may be ignoring If-None-Match entirely.
Test date-based conditionals independently
lm=$(curl -sI https://example.com/resource | grep -i '^last-modified:' | cut -d' ' -f2- | tr -d '\r')
curl -sI -H "If-Modified-Since: ${lm}" https://example.com/resource
Run this without an If-None-Match header present, since RFC 9110 §13.1.2 requires the origin to ignore If-Modified-Since whenever If-None-Match is also sent — testing both together will not tell you whether the date-based path works independently.
Inspect what a cache actually stored and revalidated
curl -sI https://example.com/resource | grep -iE 'age|x-cache|cf-cache-status'
Age: 0 immediately after a known-stale entry should have revalidated is a signal worth investigating: either the cache correctly refetched, or (if CF-Cache-Status: REVALIDATED or similar appears) it successfully validated and reused the stored body rather than refetching it wholesale.
Watch conditional requests in DevTools
In Chrome DevTools’ Network tab, a revalidated resource shows status 304 with a Size column value of a few hundred bytes (headers only) rather than the full transfer size, and the resource still renders with its previous content — confirming the browser reused its disk-cache body rather than the network response.
Common Mistakes and RFC Violations
| Anti-pattern | RFC rule violated | Fix |
|---|---|---|
Sending ETag computed after gzip/brotli re-encoding without marking it weak |
RFC 9110 §8.8.1: a strong validator must be identical for byte-identical content only | Prefix with W/ if the ETag reflects a transformed representation, or hash the pre-compression body |
Regenerating a new ETag on every response for genuinely unchanged content |
RFC 9111 §4.3 assumes a stable validator; a constantly-changing ETag defeats revalidation entirely | Base the ETag on content hash or a stable version column, not on generation timestamp |
Relying solely on Last-Modified for sub-second-changing resources |
Last-Modified has one-second resolution and cannot distinguish rapid successive edits |
Add a strong ETag for any resource that can change more than once per second |
Sending both If-None-Match and a stale If-Modified-Since and expecting both to be checked |
RFC 9110 §13.1.2 requires ignoring If-Modified-Since once If-None-Match is present |
Treat ETag as authoritative; do not rely on the date header as an additional check |
Returning 200 with a full body on every conditional request |
RFC 9111 §4.3 requires comparing the validator and returning 304 when unchanged |
Implement server-side comparison against the incoming If-None-Match or If-Modified-Since value |
Omitting Cache-Control from a 304 response |
RFC 9110 §15.4.5 requires a 304 to carry any headers that would update cache metadata |
Include the current Cache-Control (and any other changed metadata headers) on every 304 |
Using stale-while-revalidate without any validator on the resource |
Background revalidation in RFC 9111 §4.3 still requires a conditional request; without a validator it becomes a full refetch | Ensure ETag or Last-Modified is present before relying on stale-while-revalidate for latency hiding |
Frequently Asked Questions
What’s the difference between a strong and a weak validator?
A strong validator changes whenever the response body changes by even one byte and guarantees byte-for-byte equivalence between two responses that share it. A weak validator, marked with a W/ prefix on an ETag, only guarantees semantic equivalence — the response is close enough for cache purposes but may differ in incidental details. Last-Modified is always a weak validator because its one-second resolution cannot capture sub-second changes.
Why does a 304 Not Modified response have no body?
RFC 9110 §15.4.5 defines 304 as confirmation that the cached representation is still valid; it deliberately omits the body because the client already has an identical copy stored. Sending the body again would defeat the bandwidth-saving purpose of conditional requests. The cache reconstructs the full response by combining the 304’s updated headers with its previously stored body.
Does If-None-Match take precedence over If-Modified-Since?
Yes. RFC 9110 §13.1.2 requires an origin server to ignore If-Modified-Since whenever If-None-Match is present in the same request, provided the server supports the strong comparison the ETag requires. This means a client sending both headers only needs a correct ETag comparison to get a reliable result; the date-based header becomes a no-op fallback for servers that never generated an ETag.
Can stale-while-revalidate be combined with ETag validation?
Yes, and the two are complementary. stale-while-revalidate controls when a cache is allowed to serve a stale copy without waiting; the background revalidation request it triggers still uses the stored ETag in an If-None-Match header. If the origin returns 304, the cache simply extends the freshness lifetime of the body it already served; if it returns 200 with a new representation, the cache replaces the stored body for the next request.
Why would a CDN or proxy strip my ETag header?
Some reverse proxies and compression middleware regenerate the response body (for example, on-the-fly gzip or brotli re-encoding) without updating the associated ETag, so they strip it to avoid serving an incorrect validator. Others weaken a strong ETag to a weak one for the same reason. If ETag disappears between origin and client, check for an intermediate proxy, load balancer, or CDN transform rule that rewrites the response body after the origin sets the header.
Is Last-Modified obsolete now that ETag exists?
No. Last-Modified remains useful as a fallback validator for origins that cannot cheaply compute a content hash, and RFC 9110 recommends sending both when possible for maximum interoperability. Some older or minimal HTTP clients only implement date-based conditional requests, and Last-Modified also gives operators a human-readable timestamp that ETag values do not provide.
Related
- The precise formula a cache uses to decide whether a stored response is fresh in the first place — before validation even enters the picture — is covered in how to calculate cache freshness lifetime.
- Setting the TTL that determines when validation is triggered at all is covered in mastering
max-ageands-maxagedirectives. - CDN edge nodes apply the same conditional-request logic across distributed Points of Presence; see CDN architecture and edge routing strategies for how revalidation interacts with shared-cache tiers.
Back to all caching guides.