HTTP ETags and Conditional Requests: If-None-Match and 304, If-Match and 412

ETags serve both cache revalidation and lost-update prevention. Per RFC 9110 and 9111: strong vs weak comparison, If-None-Match and 304, If-Match and 412, and precondition evaluation order.

httpcacheetagconditional-requests

The ETag value in a response does two jobs. A cache uses it to ask the origin server whether a stored response is still usable, and a client uses it to keep its change from silently overwriting someone else’s when two clients edit the same resource. The first job is done with If-None-Match and 304, the second with If-Match and 412. The header names look alike, but the comparison rules and the failure responses are different.

This post follows the relevant sections of RFC 9110 (HTTP Semantics, June 2022) and RFC 9111 (HTTP Caching, June 2022). It does not cover how any particular CDN, browser or web framework behaves. Resources such as /notes/42 and the header values below are invented for illustration.

An ETag is a value the client doesn’t interpret

RFC 9110 Section 8.8.3 defines an entity tag as an opaque validator for telling apart multiple representations of the same resource, whether they exist because the resource changed over time, because content negotiation makes several valid at once, or both. The value is a quoted string, optionally prefixed with the weakness indicator W/.

ETag: "xyzzy"
ETag: W/"xyzzy"
ETag: ""

The server decides how to build the value. Section 8.8.3.1 lists an internal revision number (perhaps combined with a variance identifier for content negotiation), a collision-resistant hash of the representation content, a combination of file attributes, and a modification timestamp with sub-second resolution as examples, and says that because the value is opaque the client has no need to know how it was constructed. So a client shouldn’t see "v7", decide it is a version number and send back an incremented one, or assume it is a hash and compute its own to compare. It stores the value it received and sends that exact value back.

The same section says an origin server SHOULD send an ETag for any representation where changes can be detected reasonably and consistently. There is also a note: the old definition treated the quoted part as a quoted-string, so some recipients might perform backslash unescaping, and servers ought to avoid backslashes in entity tags.

Strong and weak ETags

RFC 9110 Section 8.8.1 defines the two kinds:

  • A strong validator changes value whenever the representation data observable in the content of a 200 response to GET changes.
  • A weak validator might not change for every change to the representation data. That happens when the way it is computed has limits (one-second clock resolution, say), or when the server wants to group representations it considers equivalent by its own rule.

Per Section 8.8.3, an ETag is strong by default, and if the way it is generated doesn’t meet all the characteristics of a strong validator, the origin server MUST mark it weak with W/. Section 8.8.1 gives an example: sending the same validator for a gzip-coded representation and an uncoded one makes that validator weak, because the representation data differs. How ETags relate to content negotiation, and the cache key used to pick a stored response, are covered separately in HTTP Vary and cache keys.

There are also two comparison functions (Section 8.8.3.2):

  • Strong comparison: two tags are equivalent if neither is weak and the quoted strings match character by character.
  • Weak comparison: two tags are equivalent if the quoted strings match character by character, whether or not either carries W/.
Tag 1 Tag 2 Strong comparison Weak comparison
W/"1" W/"1" no match match
W/"1" W/"2" no match no match
W/"1" "1" no match match
"1" "1" match match

This is Table 3 of RFC 9110. The first row is the one that matters in practice: under strong comparison, two weak tags with the same string do not match. That is why Section 8.8.1 says strong validators can be used for all conditional requests, while weak ones are only usable when the client doesn’t need exact equality with representation data it got earlier, as when validating a cache entry.

Two uses for conditional requests

RFC 9110 Section 13 defines a conditional request as one with request header fields that state a precondition to test before the method is applied to the target resource. The same section says conditional GET is the most efficient mechanism for HTTP cache updates, and that conditions on state-changing methods such as PUT and DELETE prevent the “lost update” problem, where one client accidentally overwrites the work of another client acting in parallel.

If-None-Match If-Match
Main use Cache revalidation (conditional GET) Preventing overwrites with state-changing methods
Tag comparison Weak Strong
When the condition is false 304 for GET/HEAD, 412 for other methods 412, or 2xx if the same change appears to be already applied
Who evaluates it A cache or the origin server The origin server (caches and intermediaries may ignore it)
Meaning of * True only if there is no current representation True only if there is a current representation

The basis is RFC 9110 Sections 13.1.1, 13.1.2 and 13.2.2. The two cases are taken separately below.

Cache revalidation: If-None-Match and 304

Say the client’s first exchange looked like this:

GET /notes/42 HTTP/1.1
Host: api.example.com
Accept-Encoding: gzip
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v7"
Vary: Accept-Encoding

{"text": "draft"}

Per RFC 9111 Section 4.3, when a cache can’t serve a stored response as is (because it isn’t fresh, for example), it can use a conditional request to let the next server decide; this is called validating or revalidating. If the stored response had entity tags, the cache MUST send them in the validation request (Section 4.3.1). RFC 9110 Section 13.1.2 likewise says a client that wants to update stored responses that have entity tags SHOULD send an If-None-Match listing those tags with its GET.

GET /notes/42 HTTP/1.1
Host: api.example.com
Accept-Encoding: gzip
If-None-Match: "v7"

If-None-Match uses weak comparison (MUST, Section 13.1.2), because weak tags can be used for cache validation too. If one of the listed tags matches the tag of the selected representation, the condition is false, and the origin server must not perform the method; for GET or HEAD it MUST respond 304.

HTTP/1.1 304 Not Modified
Date: Tue, 13 Oct 2026 04:05:00 GMT
ETag: "v7"
Vary: Accept-Encoding

A 304 has no content. RFC 9110 Section 15.4.5 says it ends with the header section and cannot contain content or trailers. Instead, the server MUST send whichever of Content-Location, Date, ETag, Vary, Cache-Control and Expires it would have sent in a 200 to the same request. The 304 example above includes Date.

A cache that receives a 304 picks the stored responses to update, replaces their header fields with the ones in the 304, and reuses them (RFC 9111 Sections 4.3.3 and 4.3.4). If the 304 carries a strong validator, the stored responses with that same strong validator are the ones updated, and if none has it, the cache MUST NOT use the 304 to update anything. If a full response with content comes back instead, none of the stored responses named in the conditional request is suitable, and the cache MUST use the full response to satisfy the request.

A 304 tells the client, for a read, to keep using the representation it already has. It is not a status code for signalling that a write succeeded. When If-None-Match is false on a method other than GET or HEAD, the response is 412.

Caches often send If-Modified-Since alongside it. RFC 9111 Section 4.3.1 notes that in most cases both validators are sent, even when entity tags are clearly better, so that old intermediaries that don’t understand entity tag preconditions can respond appropriately. What happens when both arrive is covered under evaluation order below.

Preventing lost updates: If-Match and 412

Here are two clients, A and B, editing the same note, traced through RFC 9110’s rules. This is not an observed incident; it applies the semantics of Sections 13 and 13.1.1 step by step.

  1. A and B each send GET /notes/42 and both receive a representation with ETag: "v7".
  2. A sends its edit as a PUT with If-Match: "v7". The current tag is "v7", so the condition is true and the server saves the content. The current tag is now "v8".
  3. B sends its own edit as a PUT with If-Match: "v7".
  4. The server compares "v7" with the current "v8" using strong comparison and finds the condition false. It does not perform the method and responds 412.

Had B sent the PUT in step 3 without a condition, B’s content would have overwritten A’s change and A would have no way of knowing. That is what the RFC calls a lost update.

A’s request and response:

PUT /notes/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-Match: "v7"

{"text": "edited by A"}
HTTP/1.1 204 No Content
ETag: "v8"

There is a condition on putting ETag in a PUT success response. RFC 9110 Section 9.3.4 says an origin server MUST NOT send a validator such as ETag in a successful response to PUT unless the content was saved without any transformation and the value reflects the new representation. If the server alters the content as it saves it, the ETag above has to go.

B’s request and response:

PUT /notes/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-Match: "v7"

{"text": "edited by B"}
HTTP/1.1 412 Precondition Failed

412 means one or more conditions in the request header fields evaluated to false on the server (Section 15.5.13). What B does next is outside the RFC. A common choice is to GET the "v8" representation, show the user the conflict or merge the changes, and resend with If-Match: "v8".

What RFC 9110 Section 13.1.1 says about If-Match, in short:

  • The origin server MUST use strong comparison, because the client wants the method not to be applied if the representation data has changed at all. It follows from the comparison table that if a server only hands out weak tags, sending one of them back in If-Match can never make the condition true.
  • If the condition is false, the origin server MUST NOT perform the method. It MAY signal the failure with 412, or, if the state-changing request appears to have already been applied, it MAY respond 2xx. The RFC’s example is a client resending a request because the earlier response was lost and it never learned that the change succeeded.
  • Allowing that 2xx is efficient, but it carries risk when several user agents send change requests that are very similar but not cooperative. The RFC’s example is a resource used as a semaphore, such as a nonatomic increment, and for those resources it says the server is better off sending 412 for every failed precondition on an unsafe method.
  • A cache or intermediary MAY ignore If-Match, since the condition only matters at the origin server.
  • With the value *, the condition is true if the origin server has a current representation of the target resource. A list mixing * with other values is syntactically invalid.

An ETag is not a lock

If-Match does not reserve the resource in advance. It compares the condition with the current state when the request arrives, and it doesn’t stop other clients from writing between A’s GET and A’s PUT. What it stops is a write based on stale state from being applied.

The server implementation needs one more thing. If A’s and B’s PUTs in the sequence above arrive at almost the same moment, the server could check “current tag is "v7"” for both requests and then save each one. Both conditions would evaluate true and one write would still overwrite the other. So comparing the tag and saving have to happen as one atomic step inside the server. That is not something the RFC guarantees; it is an inference about what an implementation needs in order to honour the RFC’s semantics. RFC 9110 Section 9.3.4 states that HTTP does not define how resource state is stored. With a database, an UPDATE conditioned on a version column or a transaction might be used, but which one is a choice made outside HTTP.

Allowing creation only: If-None-Match: *

If-None-Match: * is false if the origin server has a current representation of the target resource (Section 13.1.2). The RFC describes it as a way to keep an unsafe method such as PUT from accidentally modifying an existing representation when the client believes the resource has none yet, and calls this a variation on the lost update problem that can arise when more than one client tries to create the initial representation.

PUT /notes/43 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-None-Match: *

{"text": "first version"}
  • If /notes/43 has no current representation, the condition is true, and if the PUT creates one, the origin server MUST say so with 201 Created (Section 9.3.4).
  • If another client created it first, the condition is false, and since the method is not GET or HEAD the response is 412 (MUST, Section 13.1.2).

As in the previous section, the check for existence and the creation need to be atomic inside the server; that is the same implementation-side inference.

Date conditions and evaluation order

There are also conditions based on the last modification date. If-Unmodified-Since serves the same purpose as If-Match when the client has no entity tag (Section 13.1.4), and If-Modified-Since is used to update a cached representation that has no entity tag (Section 13.1.3). The rules for when they arrive together with ETag conditions are short:

  • If the request contains If-Match, If-Unmodified-Since MUST be ignored (Section 13.1.4).
  • If the request contains If-None-Match, If-Modified-Since MUST be ignored (Section 13.1.3). If-Modified-Since on a method other than GET or HEAD MUST be ignored as well.

The RFC explains that the entity tag condition is a more accurate replacement for the date condition, and the two are only combined to interoperate with older intermediaries that might not implement entity tag conditions.

When several conditional headers are present, they MUST be evaluated in the order given in RFC 9110 Section 13.2.2:

Step Condition Evaluated when If true If false
1 If-Match Recipient is the origin server Go to 3 412, unless it can be determined that the state change already succeeded
2 If-Unmodified-Since Recipient is the origin server and If-Match is absent Go to 3 Same as 1
3 If-None-Match Present Go to 5 304 for GET/HEAD, 412 otherwise
4 If-Modified-Since GET/HEAD and If-None-Match absent Go to 5 304
5 If-Range GET with both Range and If-Range 206 if the Range applies Ignore Range, 200
6 (none) Otherwise Perform the method and respond by its result

If-Range is outside the scope of this post and appears only in the table.

The timing is fixed too. Per Section 13.2.1, a cache or origin server MUST evaluate preconditions after it has successfully performed its normal request checks and just before it would process the request content or perform the method’s action. And if the response to the same request without the conditions would have been a status code other than 2xx or 412, the server MUST ignore all received preconditions. In other words, redirects and failures that can be detected before significant processing take precedence over evaluating preconditions.

There are rules about who evaluates them as well:

  • A server that is neither the origin server for the target resource nor able to act as a cache for the request MUST NOT evaluate these conditional headers, and MUST forward them if it forwards the request (Section 13.2.1).
  • If-Match and If-Unmodified-Since don’t apply to caches. When a cache can satisfy a request from a stored 200 or 206 response, it SHOULD evaluate applicable conditions such as If-None-Match against that stored response’s validators (RFC 9111 Section 4.3.2).
  • Conditional headers on methods that don’t select or modify a representation, such as CONNECT, OPTIONS and TRACE, MUST be ignored (Section 13.2.1).

Checks when implementing and testing

  • Does the client store and send back tag values exactly as received, without interpreting or constructing them?
  • Do tags generated in a way that falls short of a strong validator carry W/? Do the gzip and uncoded representations avoid sharing one strong tag?
  • Does If-None-Match handling use weak comparison and If-Match handling use strong comparison? The four pairs from RFC Table 3 make good test data.
  • Does a GET with a matching If-None-Match end in a 304 with no content, and a PUT with the same condition end in 412?
  • Does the 304 carry whichever of ETag, Vary, Date, Cache-Control, Expires and Content-Location the 200 would have had?
  • Does an If-Match PUT with a stale tag get 412 and leave the resource unchanged? When two PUTs with the same tag are sent at once, does exactly one succeed?
  • If you allow 2xx for a failed If-Match, is the rule for “appears to have already been applied” clear? For resources where similar but uncooperative requests pile up, like counters and increments, do you always send 412?
  • Does a PUT with If-None-Match: * get 201 when the resource doesn’t exist and 412 when it does?
  • When If-Match comes with If-Unmodified-Since, or If-None-Match with If-Modified-Since, is the date condition ignored?
  • When you add conditional headers to a request that would have ended in a redirect or in an error other than 412 without them, do you still get that redirect or error?
  • Does the ETag in a PUT success response identify the new representation exactly as stored? If the server changes content while saving, does it leave ETag out?

Sources

Checked: 2026-10-13

Back to the blog