Two clients can read the same resource, edit different fields, and send updates seconds apart. If the server accepts both writes without checking which representation each client edited, the later request can silently replace state written by the earlier one. The transport succeeded, yet the application lost a concurrent change.
HTTP provides a conditional request mechanism for this boundary. A server can attach an entity tag to a representation, and a client can return that tag in If-Match when submitting a state-changing request. The update proceeds only while the selected representation still satisfies the supplied precondition.
The validator represents server-selected state
An ETag is an opaque validator chosen by the origin server. A response can expose one alongside a representation:
HTTP/1.1 200 OK
ETag: "rev-42"
Content-Type: application/json
{"name":"Ada","role":"editor"}The client does not need to derive "rev-42" from the JSON. It stores the validator with the representation it received. The server may derive the tag from a revision number, a content digest, or another scheme, but that derivation is an implementation choice.
For concurrency control, the important property is the validator semantics, not its visual format. A strong entity tag denotes representations that are byte-for-byte equivalent under HTTP strong comparison. Weak tags, written with the W/ prefix, do not satisfy If-Match because that header uses the strong comparison function.
That distinction keeps a cache-oriented notion of semantic similarity from being mistaken for an exact update precondition.
If-Match moves the check into the write boundary
A client editing the representation above can send:
PUT /accounts/7 HTTP/1.1
If-Match: "rev-42"
Content-Type: application/json
{"name":"Ada","role":"admin"}The origin server evaluates If-Match before performing the requested method. If the current selected representation still matches "rev-42", the request can proceed. If another accepted write has already changed the representation and its validator, the precondition fails.
For a failed If-Match precondition, HTTP defines status 412 Precondition Failed. The client then has an explicit concurrency result instead of a successful response that concealed an overwritten change.
The critical implementation boundary sits between validation and mutation. An application that reads the current revision, releases its database protection, and later writes without a conditional database operation can recreate the same race inside the server. The HTTP precondition needs to map to a storage operation that preserves the condition through the write.
For a relational store, that can take the form:
UPDATE accounts
SET name = ?, role = ?, version = version + 1
WHERE id = ? AND version = ?;A zero-row update indicates that the expected version no longer identifies the current row state. The exact transaction and isolation requirements depend on the database and surrounding operations, but the comparison must not become detached from the mutation it is meant to guard.
A version token is not a lock
An ETag does not reserve the resource for a client. Two clients can hold "rev-42" at the same time. Both can prepare writes, and neither gains ownership merely by possessing the validator.
The concurrency effect appears when the server serializes or conditionally applies the mutations. One request can move the resource to a new state such as "rev-43". A later request carrying "rev-42" then fails its precondition.
This is optimistic concurrency: contention is detected at mutation time rather than prevented by holding a client-visible lock across the editing interval. It fits disconnected clients and ordinary request-response APIs because no lock lease has to survive user think time, network interruption, or process restarts.
It also means rejection is part of the normal contract. An API using this pattern needs a defined response for stale writes and a client policy for fetching current state, presenting a conflict, merging fields, or abandoning the attempted change.
PUT and PATCH expose different merge boundaries
Conditional requests do not determine update semantics. PUT and PATCH can both carry If-Match, but the meaning of their request bodies remains distinct.
A PUT representation commonly describes replacement state for the target resource. If a client constructed that representation from stale data, unconditional acceptance can overwrite fields it never intended to change.
A PATCH request describes a patch document whose semantics depend on its media type. A patch can reduce accidental replacement of unrelated fields, but it does not eliminate write conflicts. Two patches can still target the same field, or one patch can depend on state changed by another request.
If-Match places both forms behind the same representation-version boundary. Whether a server should reject any intervening change or permit finer-grained field concurrency is an application policy above HTTP. A single resource ETag intentionally treats the selected representation as one concurrency unit.
If-Match star expresses existence, not a specific revision
If-Match also accepts *. For a state-changing request, that form succeeds when the origin server has a current representation for the target resource. It does not assert that the resource has a particular version.
This makes the two forms materially different:
If-Match: "rev-42"requires a strong match with the supplied entity tag, while:
If-Match: *expresses a condition tied to the existence of a current representation.
An API that needs stale-write detection therefore normally returns and consumes a specific strong validator. Replacing that token with * discards the revision identity that carries the concurrency check.
Validator scope has to match mutation scope
A validator is useful only for the state it actually represents. If an API returns an ETag derived from a subset of fields but permits the same endpoint to mutate additional state, a successful comparison may not detect changes outside that subset.
The opposite mismatch can also be costly. A validator covering unrelated volatile metadata can reject edits that do not conflict at the application level. Such rejection is still safe from a lost-write perspective, but it can create unnecessary contention.
The design question is therefore the concurrency unit: which server state must remain unchanged between read and write for the proposed mutation to be valid? The ETag generation policy, representation shape, and storage predicate should describe the same boundary.
Conditional writes make conflicts observable
Without a precondition, two successful HTTP responses can hide a lost update. With a strong validator and a storage-level conditional mutation, the second stale writer receives an observable failure before its state replaces the accepted revision.
That mechanism does not merge concurrent edits, choose a conflict winner, or define business policy. It converts an implicit race into an explicit interface result. The useful contract is narrow: a mutation applies only to the representation revision the client names, and a changed revision forces the caller onto a separate conflict path.