Two clients can read the same resource, make different edits, and then save them seconds apart. Without a concurrency check, the later write can silently replace the earlier one. This is the lost update problem.
HTTP already provides a protocol-level mechanism for avoiding that failure: validators such as entity tags (ETags) combined with conditional request headers. Used correctly, they let a client say, “apply this change only if the resource is still the version I read.”
The lost update problem
Consider a document resource whose current state is version 7.
Client A: GET /documents/42 -> version 7
Client B: GET /documents/42 -> version 7
Client A: PUT /documents/42 -> version 8
Client B: PUT /documents/42 -> version 9If both PUT requests are unconditional, Client B can overwrite Client A’s changes without knowing that the resource changed after B read it.
Applications sometimes address this by putting a version number in the JSON body. That can work, but HTTP preconditions provide standard request semantics that intermediaries, clients, logs, and API tooling can understand.
ETags identify representation versions
An origin server can attach an ETag response header to a selected representation:
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "doc-42-v7"
{"id":42,"title":"Draft"}An entity tag is an opaque validator. Clients should store and return it as defined by the protocol rather than infer meaning from its internal text.
The tag does not have to contain a database version. A server might derive it from a revision identifier, content metadata, or another mechanism that changes whenever the representation data relevant to the validator changes.
The important property for concurrency control is that the validator accurately distinguishes the state the client read from a conflicting newer state.
Use If-Match for state-changing requests
After reading the resource, the client sends the received tag in If-Match when it attempts an update:
PUT /documents/42 HTTP/1.1
Content-Type: application/json
If-Match: "doc-42-v7"
{"id":42,"title":"Reviewed draft"}The origin server evaluates the precondition before performing the method. If the current representation still has the matching entity tag, the update can proceed.
If another writer has already changed the representation, the old tag no longer matches. The server must not perform the requested method because of that failed If-Match condition. A 412 Precondition Failed response is the normal way to tell the client its assumption about resource state is no longer true.
A failed precondition is not a server crash
412 Precondition Failed describes a concurrency outcome, not an unexpected internal error. The client should usually fetch or otherwise obtain the current state, decide how to reconcile its intended change, and then submit a new request using a validator for that state.
Blindly retrying the same stale request with the same If-Match value cannot make the precondition become true unless the resource happens to return to a matching state.
Strong and weak ETags are different
HTTP defines strong and weak entity tags. A weak tag is marked with the W/ prefix:
ETag: W/"summary-18"Weak validators can be useful for cache validation when two representations are equivalent enough for that purpose even if their representation data is not byte-for-byte identical.
If-Match, however, uses the strong comparison function. Two tags compare strongly only when neither is weak and their opaque tag values match. A weak ETag therefore cannot satisfy an If-Match strong comparison.
For optimistic concurrency control with If-Match, provide a strong validator whose semantics are appropriate for detecting the changes that must block the write.
Keep the check and write atomic
Adding an If-Match header to the API is not enough if the server implements the check incorrectly.
This sequence is unsafe:
1. read current database version
2. compare it with If-Match
3. another transaction updates the row
4. write the new value unconditionallyThe application has recreated a race between the validation and the write.
The persistence layer must make the version check part of the same concurrency-controlled operation as the update. Depending on the datastore, that might mean a conditional update, compare-and-swap operation, transaction with suitable locking, or another atomic concurrency mechanism.
HTTP supplies the client-visible precondition semantics. The storage system must enforce those semantics without a check-then-write race.
Generate validators from the state you actually protect
An ETag used for write concurrency should change when a relevant conflicting modification occurs.
Suppose a response includes a document plus a computed viewCount. If changes to viewCount should not prevent someone from saving the document title, deriving the write validator from the entire serialized response can create unnecessary conflicts.
Conversely, a validator based only on the title would be insufficient if changing the document body must also invalidate an outstanding edit.
Define the concurrency boundary first, then generate the validator from a revision that represents that boundary.
Representation selection matters
A resource can have multiple representations. Content negotiation, compression, language selection, or API projections can affect what representation is selected.
Do not assume that an arbitrary hash of response bytes automatically gives the application the write-concurrency semantics it needs. Validator generation and comparison must follow HTTP semantics and remain consistent with how the origin server selects representations.
For many APIs, a stable resource revision identifier is easier to reason about than hashing whatever bytes happen to be serialized for one response.
If-Match star means the resource must exist
If-Match: * has a useful but different meaning:
DELETE /documents/42 HTTP/1.1
If-Match: *The condition is true when the origin server has a current representation of the target resource. It does not mean “match any previous version I might have read.”
Use a specific strong entity tag when the operation must be tied to a particular observed version. Use * when the relevant condition is existence itself.
If-None-Match solves a different condition
If-None-Match reverses the test. With *, it can protect a create-if-absent operation:
PUT /documents/42 HTTP/1.1
Content-Type: application/json
If-None-Match: *
{"id":42,"title":"First draft"}For a method other than GET or HEAD, a false If-None-Match condition results in 412 Precondition Failed. This allows a client to request creation only when a current representation does not already exist.
For GET and HEAD, If-None-Match is primarily a cache-validation mechanism. A matching validator leads to 304 Not Modified rather than 412.
Do not interchange If-Match and If-None-Match: they express opposite preconditions and have different common uses.
Prefer entity tags over timestamps for precise concurrency
HTTP also defines date-based preconditions such as If-Unmodified-Since. They are useful when a resource does not provide entity tags, but dates have limitations as concurrency validators.
HTTP dates have one-second granularity. Multiple changes can occur within the same second, and modification times may not represent every state transition that matters to an application.
When an API can issue reliable entity tags, If-Match generally gives a more precise way to tie a write to an observed resource version.
Decide whether preconditions are optional or required
An API that supports If-Match but also accepts unconditional updates still permits lost updates from clients that omit the header.
For resources where accidental overwrites are unacceptable, define the API contract so state-changing requests are expected to carry the appropriate precondition. Enforce that policy consistently rather than relying on every client developer to remember it.
HTTP defines 428 Precondition Required for servers that require a request to be conditional. It is defined in RFC 6585, not in the core HTTP semantics document.
A useful distinction is:
428 Precondition Required: the server requires a precondition, but the request did not provide the required one;412 Precondition Failed: a supplied precondition was evaluated and was false.
Return the new validator after a successful update
After a successful modification, clients often need the validator for the resulting representation so their next edit can be conditional on the new state.
If the response contains an ETag, make sure it corresponds to the representation associated with that response according to HTTP’s validator rules. Do not echo the client’s old tag after the resource has changed.
Some APIs return the updated representation; others return a minimal success response and require a subsequent read. Choose deliberately based on payload cost and client workflow.
Handle conflicts as a product decision
Detecting a stale write is only the first step. The application still needs a conflict policy.
Possible client behaviors include:
- show the latest server state and ask the user to reapply edits;
- merge non-overlapping fields when the domain supports a safe merge;
- discard the local edit after explicit user confirmation;
- re-run a domain command against the new state instead of replaying a stale full-resource replacement.
Automatic merging is not universally safe. A syntactically mergeable change can still violate business intent.
For APIs with commands such as “approve invoice” or “reserve seat,” domain-specific preconditions can matter more than blindly replacing a JSON document.
Common implementation mistakes
Treating the ETag as a client-generated version
The validator belongs to the representation selected by the server. Clients should return the received value, not construct a guessed next version.
Comparing weak tags with If-Match as if they were strong
If-Match requires strong comparison. A weak validator is not a substitute for a strong concurrency token.
Checking the version outside the write transaction
A separate read and later unconditional write leaves a race window. Enforce the expected version atomically with the mutation.
Mapping every write conflict to 409
409 Conflict has legitimate uses, but a failed HTTP precondition already has a specific status: 412 Precondition Failed. Preserve that distinction when the failure is specifically an evaluated If-Match or other HTTP precondition.
Retrying stale full-resource writes automatically
A retry with a newly fetched validator but the same stale replacement body can simply overwrite the intervening change after bypassing the protection. Reconciliation must happen before resubmission.
A practical request flow
A straightforward optimistic-concurrency flow is:
- the client retrieves a resource and stores its strong ETag;
- the client prepares an edit based on that representation;
- the client sends the state-changing request with
If-Match; - the server atomically checks the expected revision while applying the mutation;
- on success, the server returns the resulting state or its new validator;
- on a failed precondition, the client obtains current state and reconciles instead of blindly overwriting it.
This pattern does not eliminate concurrent edits. It makes them visible before data is silently lost.
HTTP conditional requests are most valuable when their protocol semantics and the database’s concurrency mechanism describe the same rule. A strong ETag tells the client which state it observed, If-Match carries that expectation back to the server, and an atomic conditional write ensures the expectation is actually enforced.