pattern

Conditional Request

also called If-Match Write, ETag Precondition, HTTP Optimistic Concurrency

An HTTP request that carries a validator for the version the client last saw, so the server can refuse a write built on a stale read instead of silently discarding someone else's change.

httpetaglost-updatepreconditionsconcurrency

Two people open the same record, each edits a different field, and both save. The second save carries a payload assembled from a snapshot taken before the first one landed, so it asserts the old value of the other person's field. The server applies exactly what it was asked to apply, nothing errors, and the first change is gone. This is the lost update, and it is invisible in logs, metrics and tests.

A conditional request fixes it in the protocol rather than in each handler. The read returns ETag: "v41", an opaque validator for that representation. The write sends If-Match: "v41". The server compares the validator before applying anything and returns 412 Precondition Failed if the resource has moved on. A silent overwrite becomes a visible conflict the client resolves by re-reading.

Why it matters

Read-modify-write over a stateless API has no built-in notion of "the version I was looking at". Without a validator, last-writer-wins is not a policy anyone chose; it is the default, and it fails most often in exactly the workflows where accuracy matters, where several people or several automated jobs touch the same record.

The mechanism is cheap, standardised (RFC 9110, 2022, section 13) and understood by proxies and caches, which matters because the same validator also powers If-None-Match for cache revalidation. One field on the read buys both concurrency control and conditional caching.

Implementation patterns

  • Derive the validator from something that changes on every write: a monotonic row version, or a hash of the stored representation. A timestamp with one-second resolution cannot tell two writes in the same second apart, which is the case you are defending against.
  • Require the precondition, do not merely honour it. Reject an unconditional write on a mutable resource with 428 Precondition Required (RFC 6585). A precondition that only applies when the client remembers it protects the clients who did not need protecting.
  • Migrate in four steps: log writes with no If-Match by API key, warn in the response, enforce per client, then enforce globally with a dated deadline. The log almost always shows one unowned script.
  • Return the new ETag on the write response so a client making consecutive edits does not need a re-read between them.
  • Scope the validator to the representation, not the row. If two media types or two field projections of the same resource share a validator, a client that read one and writes the other gets a false conflict or a false success.
  • For collections, prefer per-item validators. A validator over a whole list conflicts on every unrelated change and trains clients to retry blindly.

Industry example

The mechanism is defined in HTTP itself rather than by a single vendor: ETag, If-Match, If-None-Match and the 412 and 428 status codes are specified in RFC 9110 (2022) and RFC 6585 (2012), and cloud object stores and document databases expose the same idea under names like version numbers and sequence tokens on compare-and-set writes. The pattern's ubiquity is the argument for using it: an integrator who has written one HTTP client has already met it, which is not true of a bespoke expected_version field in your request body.

Failure scenarios

  • Weak validators used for writes. A weak ETag (prefixed W/) asserts semantic equivalence, not byte equality, and is for caching. Using one for If-Match can allow a write through after a change the server considered insignificant.
  • A validator that does not change because it is computed from a last_modified column the write path forgets to touch. Every conditional write then succeeds and the protection is theatre.
  • Clients that retry a 412 unchanged, re-sending the same stale payload in a loop. The 412 body must say what to do: re-read, re-apply, re-submit.
  • Proxies stripping or rewriting the header, which turns enforcement into an intermittent 428 that only some customers see.

Trade-offs

Choose conditional writes Gains Pays
On any multi-writer mutable resource Lost updates become 412s; conflicts become visible and resolvable One extra round trip per conflict, and client code that must re-read and merge
Over a database row lock No lock held across human think time; no abandoned transactions Conflicts are detected late, after the user has done the work
Over a hand-rolled version field Enforced by the protocol and visible to intermediaries The validator becomes part of your public contract and cannot change format freely

When not to use it

For an append-only resource there is nothing to lose. For an idempotent set-to-a-known-value write whose value does not depend on what was read, the ceremony buys nothing and the extra round trip is pure cost. For genuinely exclusive workflows such as a stock count or a payroll run, pessimistic locking with an explicit short lease is the better fit, because detecting the conflict after ten minutes of work is worse than preventing the second editor from starting. And where concurrent edits are semantically additive, model the operation as the addition (add a tag, increment a counter) and the conflict disappears instead of being reported.

Interview question

Q: Your public API has honoured If-Match for years but never required it. Finance reports that a monthly reconciliation keeps finding customer records whose address reverted. How would you find out whether this is a lost update, and how would you move to enforcement without breaking the integrators who never sent the header?

What a strong answer covers: instrumenting unconditional writes per API key to size the problem before changing behaviour · distinguishing a lost update from a replay or a bad merge by looking at write pairs within one validator generation · the four-step migration with a dated deadline and a brownout · what the 412 body must tell a client · and recognising that PATCH would reduce but not remove the race, so partial updates and concurrency control are separate decisions.

Quick check

Quiz: A client sends a write with no If-Match to a mutable resource. What should the server return, and why is applying it worse than rejecting it? — 428 Precondition Required; applying it leaves the resource with no concurrency invariant at all, because the one client that skips the check can overwrite every careful client.

Flashcard: What must an ETag be derived from for conditional writes to be safe? — Something that changes on every write, such as a row version or a hash of the representation; a one-second timestamp cannot separate two writes in the same second.