skip to content

Optimistic Concurrency with ETag/If-Match

Preventing lost updates over HTTP: clients send If-Match with the ETag they read, and the server rejects stale writes with 412. Interviewers ask because it is the canonical REST answer to 'two users edit the same record — what happens?'.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

3

A client's write to your API is rejected with HTTP 412 because the version token it sent was stale. Walk through what the client should do next, and what the API has to provide for that step to be possible.

level: middleimportance: should knowfreq 40%

answer

  1. Read → merge → retry, bounded
  2. Server never auto-resolves a 412
  3. Disjoint fields = auto-merge; overlap = ask the user
  4. Return the new ETag on every successful write
  5. 412 is not a generic retryable error

basics

~20 s

Re-read the resource to get current state and a fresh validator, reconcile the user's intended change against what actually changed, then resend with the new validator. Never blindly resend the same body with a refreshed token, and cap the retry loop.

solid answer

~50 s

The loop is **read → merge → retry**, bounded. 1. **Re-read.** `GET` the resource; take the current representation and the new `ETag`. 2. **Reconcile.** Diff what the user changed against what the server changed. Disjoint fields → reapply automatically. Overlapping fields → a genuine conflict, so a human decides: show both values, or reload with a warning if the edit was trivial. 3. **Retry once with the fresh validator**, capping at two or three attempts so a hot resource cannot spin forever. On the API side: the 412 needs a typed error code so clients can branch on it; every successful write should return the new `ETag` so sequential edits need no extra `GET`; and `PATCH` helps enormously, because sending only changed fields makes the auto-merge case trivial. What the API must **not** do is retry on the client's behalf — that reapplies stale data and defeats the precondition.

go deeper

for a junior

Say the client re-fetches the resource, gets the new ETag, reapplies the change and retries, and that the server does not resolve it for you.

for a middle

Describe three-way merge against the original base, the disjoint-vs-overlapping distinction, and bounding the retry count.

for a senior

Add API-side obligations — typed error codes, returning fresh ETags on writes, PATCH support — and the UX design of the conflict moment.

for a principal

Talk about conflict rate as a product metric, when to invest in automatic merge or collaborative editing instead, and setting a house convention so every client handles 412 identically.

## Why the client, not the server, resolves it A 412 says the server refused to guess. The write was built from a version of the resource that no longer exists, and only the caller knows whether the user's intent still holds against the new state. Any server-side auto-retry ("just take the current version and apply the body") converts the precondition into a no-op and restores the lost update it was added to prevent. ## The three steps **Re-read.** Issue a fresh `GET`, which yields current state plus a current validator. Some APIs include the current representation in the 412 body to save the round trip; that is a legitimate optimization, but it bloats conflict responses and can expose fields the caller is not entitled to, so a plain re-read is the common default. **Merge.** Compute three-way: the base you originally read, your local edit, and the server's current state. Three outcomes: - *Disjoint* — you changed `note`, the server changed `status`. Reapply your change to the new base automatically; the user sees nothing. - *Overlapping but equal* — someone already made your change. Treat as success; do not write. - *Overlapping and different* — a true conflict. A machine cannot pick; surface both values and let the user choose, or for trivial edits discard and reload with a clear message. Silently preferring the local copy is a lost update in slow motion. Auto-merge is only safe when you know precisely which fields the user touched, which is why clients that keep the originally read representation as a base, and APIs that accept `PATCH` with just the changed fields, do far better here than clients that `PUT` an entire form. **Retry.** Resend with the new validator. Bound the loop — two or three attempts — and fall back to a user-visible error. On a hot resource an unbounded loop can livelock, with a client permanently one version behind. Add a small randomized delay between attempts when several clients contend. ## What the API must supply - **A typed error.** A problem document with a stable code such as `stale_resource` lets clients branch reliably; a 412 with an empty body forces guesswork. - **A fresh validator on every successful write.** Returning the new `ETag` on the 200/204 means a client performing several sequential edits never re-`GET`s, removing most conflict opportunities outright. - **`PATCH` support** for partial edits, so a merge is field-scoped rather than whole-document. - **Documented conflict semantics.** State plainly that the client re-reads and retries, that the server never auto-resolves, and how many retries are reasonable. ## UX consequences worth naming Optimistic concurrency does not eliminate conflicts, it *relocates* them from silent data loss into a moment the user experiences. If that moment is a raw error toast, users will hate the feature and product will ask you to drop the precondition. Design the disjoint-field auto-merge path first, keep the human-visible conflict for genuine overlap, and preserve the user's unsaved input across the reload — losing what they typed while telling them you prevented data loss is a bitter joke. ## Distinguishing conflict from other failures A stale validator is not a validation error, not a permission error, and not a transient failure. Do not lump 412 into a generic retry-with-backoff bucket in your HTTP client: retrying the identical request with the identical stale token fails forever. It needs its own branch that goes back through the read step.

  • Should the server include the current representation in the 412 response body?
    It is a valid optimization that saves the client a GET, and some APIs do it. The costs are a larger response on a path that can spike under contention, possible exposure of fields the caller cannot otherwise read, and duplicated serialization logic. Most APIs keep the 412 body to a typed error and let the client re-read.
  • How does supporting PATCH change the conflict experience compared to PUT?
    With PATCH the client sends only the fields it changed, so after a 412 the merge is field-scoped: if the server's change touched different fields, the same patch can simply be replayed against the new validator. With a full PUT the client must reconstruct the whole document from the new base before retrying, and a naive replay silently overwrites the other writer's fields.

saying these in an interview costs you the question

  • Resending the same body with the newly fetched validator and calling it a merge
  • Having the server transparently retry the write with the current version
  • Retrying a 412 in an unbounded loop, or lumping it into the generic transient-retry path
  • Presenting every conflict to the user even when the changed fields do not overlap
  • Discarding the user's unsaved input when reloading after a conflict

context

open as a page

Your API currently accepts unconditional PUT requests, and you want every write to a sensitive resource to carry a precondition, rejecting the ones that do not with HTTP 428 Precondition Required. How would you roll that out across existing clients?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Decide which resources genuinely need it, ship the ETag on reads first, then run a warn phase where unconditional writes are accepted but counted per client. Once that count hits zero for the resources you are enforcing, flip them to 428 behind a flag with a documented, actionable error.

open as a page

You are adding ETag-based preconditions to a resource with many independently edited fields and embedded sub-objects. How do you decide what the ETag value should be computed over, and what breaks if you choose badly?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Compute the validator over exactly the state the endpoint protects. Whole-resource validators are simple but cause false conflicts when clients edit unrelated fields; finer granularity means exposing those parts as their own resources with their own ETags, not narrowing the token on the same URL.

open as a page