A client reads a resource at revision 41, edits one field, and sends the whole representation back. During that interval, another client commits revision 42. If the server accepts the first client’s replacement without testing its source revision, revision 42 can disappear from the visible state even though both requests completed normally.
This is the lost-update shape at an HTTP boundary. The notable detail is not simultaneous execution. The requests can arrive seconds apart. The conflict exists because a later mutation was derived from an earlier representation and the server has no condition connecting those two facts.
HTTP already has a protocol mechanism for expressing that condition. A strong entity tag identifies a representation version for comparison purposes, while If-Match makes a request conditional on that validator still matching the current representation. The pair turns a state-changing request from an unconditional command into a compare-and-mutate operation at the resource boundary.
A successful write can still erase a concurrent change
Consider a resource returned as:
HTTP/1.1 200 OK
ETag: "rev-41"
Content-Type: application/json
{"name":"Ada","quota":10,"region":"eu"}Client A changes quota. Client B, from the same starting representation, changes region. If both clients send complete replacements and the server applies them in arrival order, the second replacement contains an old value for the field changed by the first.
The final state depends on request order rather than on an explicit decision about conflicting edits. Database transactions do not automatically remove this problem. Each HTTP request can execute inside a valid transaction and still overwrite state produced by a previous transaction. The missing constraint sits above transaction atomicity: the mutation has to be conditional on the version from which it was derived.
This distinction matters for API design. An endpoint can be free of torn writes and still permit lost updates. Atomic storage protects the integrity of each individual mutation; version preconditions protect the relation between an earlier read and a later mutation.
If-Match carries the observed version back to the server
With a validator from the read response, the mutation can state its dependency directly:
PUT /accounts/17 HTTP/1.1
If-Match: "rev-41"
Content-Type: application/json
{"name":"Ada","quota":20,"region":"eu"}Under HTTP semantics, an origin server evaluates If-Match before performing the method. Entity tags supplied in If-Match use strong comparison. If the current representation no longer matches "rev-41", the precondition fails and the mutation is not applied, apart from the protocol case in which the server can establish that the requested state change already succeeded.
A typical failed comparison is represented by 412 Precondition Failed. That response changes the concurrency contract. The server no longer has to guess whether stale input is acceptable. It reports that the state assumed by the request is no longer current.
The validator is opaque to the client. "rev-41" could be backed by a database revision, a content digest, or another mechanism that satisfies the server’s validator semantics. Clients compare and return the value; they do not need to interpret its internal structure.
Strong validators are part of the correctness condition
HTTP distinguishes strong and weak validators. A weak entity tag can support cache validation when two representations are considered equivalent enough for that purpose, but If-Match requires strong comparison. Two weak tags do not match under that comparison even when their opaque text is identical.
That rule follows from the role of the precondition. A write guard is not asking whether cached content is reusable. It is asking whether the representation on which a mutation depends is still the current representation for the comparison being made.
A server therefore cannot take an arbitrary cache validator and assume it is suitable for mutation control. If an entity tag is advertised as strong, its generation has to meet the strong-validator requirements for the selected representation. In particular, a change to representation data observable in a successful retrieval requires a different strong validator.
There is also a representation-selection boundary to consider. Content negotiation can produce multiple representations of one resource. A validator scheme has to distinguish representation data correctly across those variants. A revision token copied directly from a storage row can be adequate in one API and incomplete in another if different negotiated representations can change independently or encode materially different bytes.
The storage comparison has to be atomic with the mutation
An HTTP precondition is only as effective as its server-side enforcement. This implementation has a race:
read current version
compare with If-Match
perform unrelated work
update rowIf another writer can modify the row between the comparison and the update, both requests can pass the check against the same old version. The protocol condition was parsed correctly but not preserved through the storage boundary.
A common relational form combines comparison and mutation:
UPDATE account
SET quota = :quota,
region = :region,
version = version + 1
WHERE id = :id
AND version = :expected_version;The affected-row count then carries concurrency information. One updated row means the expected version matched at the point of mutation. Zero rows means either the resource did not match that version or another condition prevented the update; the application has to distinguish those cases if its API contract requires distinct responses.
Equivalent protection can come from a transaction with suitable locking or another storage primitive that makes the version test and state transition indivisible relative to competing writers. The exact mechanism is storage-specific. The invariant is not: a request accepted on validator V must not commit after another accepted mutation has already moved the guarded resource away from V.
PUT and PATCH expose different conflict surfaces
Conditional requests are often discussed with PUT because complete replacement makes lost updates easy to see. The same version dependency can exist with PATCH.
A patch that changes only /quota does not necessarily conflict semantically with a concurrent patch to /region. Yet If-Match against a resource-level entity tag will reject the second mutation once the first changes the representation. That behavior is conservative: the validator guards the representation as a unit, not an application-specific notion of independent fields.
This is not a protocol defect. It exposes a modeling choice. If independent subresources are intended to have independent concurrency domains, representing them as separate resources can provide separate validators. If the resource is deliberately one consistency unit, rejecting a stale patch can be appropriate even when a particular pair of edits could have been merged.
Automatic merge logic changes the contract again. A server that can establish field-level compatibility may accept operations based on richer domain rules, but then the merge semantics belong to the application protocol. If-Match itself only answers whether the supplied validator matches current representation state.
Preconditions do not make every mutation idempotent
Version guards and idempotency address different failure modes.
If-Match constrains the state against which a mutation may run. It can stop stale state from overwriting a newer representation. It does not, by itself, give a non-idempotent operation safe replay semantics after a client loses the response.
Suppose a POST operation increments a counter and the response disappears in transit. Repeating the same request with a precondition might fail after the first increment changed the validator, which can prevent a second increment in some designs. But that outcome depends on the resource and operation. It is not a general duplicate-request protocol.
Likewise, an idempotency key can deduplicate repeated submissions while doing nothing about a stale read-modify-write cycle. The two mechanisms can coexist because they guard different dimensions: one identifies a request attempt across retries; the other binds a mutation to observed resource state.
Creation has a related but distinct precondition
If-Match: * means the condition succeeds when the origin server has a current representation of the target resource. It is useful when a mutation is valid only if that resource already exists.
The inverse creation case uses If-None-Match: *. For an unsafe method such as PUT, that precondition can express “create only if no current representation exists.” Two clients racing to create the same target no longer need an application-specific read-before-create sequence to express the condition at the HTTP layer.
The storage implementation still needs an atomic existence constraint. A precondition evaluated against a stale application cache followed by an unconstrained insert can recreate the same check-then-act race. A uniqueness constraint, conditional write primitive, or equivalent transactional mechanism has to carry the condition through the commit point.
Date validators are a weaker fit for precise write guards
If-Unmodified-Since can also make a state-changing request conditional on modification time when no entity tag is available. HTTP gives If-Match precedence when both fields are present.
Modification times can be unsuitable as exact version identities when their resolution permits multiple changes within one represented interval or when the server cannot treat the timestamp as a strong validator. Entity tags avoid exposing time as the comparison model and allow the origin server to choose a validator suited to its representation semantics.
For concurrency control, that opacity is useful. The API contract needs a stable comparison token, not a client-visible clock model.
The API boundary becomes an explicit compare operation
Without a precondition, a request such as PUT /accounts/17 can mean “replace whatever is there now with this representation.” With If-Match, it can mean “replace the current representation only if it is still the one identified by this validator.”
That small protocol addition changes where concurrency is visible. A conflicting edit stops being an accidental consequence of arrival order and becomes a failed condition that the caller can observe. The server still needs correct storage synchronization, validator generation, and resource modeling, but those concerns now meet at a precise boundary: the version observed by the client must still be the version authorized to change.