An ETag can let a client say “apply this edit only to the version I read.” A strong validator with If-Match, checked atomically with the write, prevents a stale client from silently overwriting a newer representation. A failed condition should lead to conflict recovery that preserves the local draft. Cache revalidation, unconditional retries, and a database check performed separately from the update do not provide the same guarantee. This article traces the complete design from HTTP exchange to storage and user interaction.
Two people can save a valid edit and still lose work. Each reads the same document. One changes its shipping address, and the other changes a contact name. If both submit a complete replacement, the second save can put the old address back while successfully changing the name. Every field remains syntactically valid. The defect is that the replacement was built from a state that no longer exists.
This is the lost-update problem in its most recognizable form. The server needs a way to distinguish “replace the version I saw” from “replace whatever happens to exist now.” The distinction must survive browser tabs, mobile caches, retries, and concurrent requests reaching different application instances.
HTTP already supplies conditional request vocabulary. The engineering challenge is to make that vocabulary correspond to a real storage condition and a usable conflict flow. The worked resource below is hypothetical, and the wire exchanges illustrate standard semantics rather than a framework-specific SDK. The controlling sources are the HTTP standards, checked for this article on October 10, 2026.
Reconstruct the overwrite before changing the API
At 09:00, Maya and Owen read customer record 417. The selected JSON representation contains an address and a contact name. Both clients store the same base data. At 09:02, Maya changes the address and saves. At 09:03, Owen changes the name and saves the entire record.
The server might validate both requests, verify that both people have permission, and commit both writes successfully. None of those checks establishes whether Owen intended to revert Maya’s address. Looking only at the final request body cannot recover that intent because it contains a mixture of the new name and the old address.
An initial fix is to make the client send only changed fields. That reduces the collision surface, but it does not eliminate conflicts. Maya and Owen can edit the same field. More subtly, two different fields can have a shared invariant: a shipping country and postal code, a start date and end date, or an account status and approval reason. Disjoint keys do not necessarily mean independent decisions.
The API therefore needs a declared concurrency boundary. A single customer representation can be one boundary; independently editable subresources can be separate boundaries if their invariants permit it. Choosing a boundary is a domain decision. A whole-record boundary rejects more harmless concurrent edits, while a narrow boundary risks accepting combinations that nobody reviewed together.
Before implementing validators, replay the observed overwrite using two requests with the same initial state. Record the expected outcome: the first edit succeeds, the second reports a stale base, and Owen’s unsaved name remains recoverable. That outcome is specific enough to guide both the HTTP contract and the interface. “Support ETags” alone is too vague to establish whether the lost work has been addressed.
The smallest useful conditional exchange
The origin returns a validator with the representation. The client retains that validator alongside the data it edits. Its replacement request supplies the validator in If-Match. Under RFC 9110, that field uses strong comparison, and a failed precondition normally produces 412 Precondition Failed. The following exchange applies those documented semantics to the hypothetical customer resource. RFC 9110, conditional requests.
GET /customers/417 HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "customer-417-r12"
{"address":"14 Harbor Road","contact":"Owen"}
PUT /customers/417 HTTP/1.1
Content-Type: application/json
If-Match: "customer-417-r12"
{"address":"29 Market Lane","contact":"Owen"}
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "customer-417-r13"
{"address":"29 Market Lane","contact":"Owen"}
Owen’s later request still carries the validator for revision twelve. The resource has moved to revision thirteen, so the server rejects that condition instead of applying Owen’s complete replacement. A product can return a small application error body explaining how to recover, provided it does not confuse a rejected write with a successful save.
The validator should travel with the snapshot, not live in an unrelated global variable. A client can display one resource while a background fetch updates another cache entry. If it sends the newest validator with an old draft, it falsely asserts that the draft was based on the new representation. That can defeat the protection even though the request contains a well-formed header.
On success, capture the returned state and validator as the new editing base. On failure, retain the original base and the local draft. These three objects—base, local draft, current server state—are the raw material for conflict recovery. Deleting any of them makes it harder to determine what the user changed and what another writer changed.
Give the validator a precise meaning
A strong ETag represents the selected representation closely enough for strong validation. A weak tag, marked with W/, does not satisfy strong comparison for If-Match. RFC 9110 defines the comparison rules and representation validator requirements; the client must treat the tag as opaque rather than deriving a revision from its appearance. RFC 9110, entity tags.
For the illustrative API, a revision counter is understandable: any relevant write advances it. But the implementation has to define “relevant.” If the JSON includes a computed status derived from another table, changing that table can change the representation without advancing the customer row’s counter. The validator design must reflect the selected representation, or the advertised guarantee becomes incomplete.
A content hash offers another possible design, but hashing arbitrary serialization creates its own questions. Does key ordering change? Are insignificant spaces stable? Are access-dependent fields included? Are two content encodings being treated as the same byte representation? An engineer should settle those choices before calling a tag strong. A convenient label is not sufficient evidence about what changed.
Consider an API that returns a full customer representation to an administrator and a redacted representation to a support agent. The returned validators belong to those selected representations. A support client should not obtain hidden data by guessing a tag or assume that a tag authorizes a write. Authorization remains an independent check.
There is a tension between strict representation identity and domain concurrency. A background update to a visible “last viewed” timestamp could invalidate every editing session. The better design may remove that volatile field from the editable representation or place it in a separate resource. Weakening validation to ignore arbitrary changes hides the tension rather than resolving it.
Validator generation should also remain stable across application instances. If each instance derives a tag from its own clock or cache state, two servers can disagree about the same committed data. A shared durable revision, or a deterministic representation-based validator, makes the condition meaningful across the actual write path.
Put the comparison inside the write boundary
The dangerous implementation reads the current revision, compares it in application code, and then issues an unconditional update. Two requests can both pass the comparison before either commits. The presence of If-Match does not make those operations atomic.
Imagine the storage revision is twelve. Request A reads twelve; request B reads twelve. Both compare successfully. A writes revision thirteen. B then writes its old replacement as revision fourteen. The server has recreated the lost update behind a standards-shaped interface. A sequential integration test may pass because it never exercises that interval.
A proposed storage design makes the revision condition part of the mutation: update this resource only while its stored revision is the expected value, and advance the revision in the same atomic operation. Another design uses a transaction that serializes the relevant read and write. The appropriate mechanism depends on the database and its documented concurrency behavior; no specific database syntax is assumed here.
The application must inspect the mutation outcome. If no record satisfies the expected revision, a follow-up determination can distinguish a missing resource from a stale resource according to the API contract. Do not return success merely because the update command itself executed without a transport error.
Related records complicate the boundary. A customer edit may also write an address table and an audit event. If acceptance depends on their joint state, they need a transactional or otherwise explicitly coordinated design. A validator on the customer row cannot magically protect an invariant spread across independent systems.
External side effects should occur only under a design that handles commit outcomes. Sending a notification before discovering that the conditional write failed produces a message about a change that never happened. Sending it after a commit can still duplicate under retries unless its delivery has its own identity and recovery semantics. The ETag solves the stale representation condition; side-effect delivery remains a separate problem.
The useful implementation review question is concrete: can two writers that observed the same validator both commit incompatible replacements? If the answer is yes, the system has not achieved the intended lost-update guarantee, regardless of how many header tests pass.
Require a condition when the resource needs one
An optional If-Match policy protects cooperative clients but leaves old or buggy clients able to overwrite unconditionally. For a resource whose edits must always be conditional, the server can require a precondition. RFC 6585 defines 428 Precondition Required for that purpose and specifies that such responses must not be stored by a cache. RFC 6585, section 3.
Requiring conditions changes the API contract. Existing integrations may send complete replacements without validators. The migration plan should identify them and give them a read-edit-write path before enforcement. Silently generating a current validator on the server for an unconditional request does not preserve the caller’s original base; it merely disguises the same overwrite.
An emergency overwrite operation can be legitimate in some products, but it should express a deliberate current decision. A user who has inspected the latest state can issue a new conditional replacement against that state. Calling a button “force save” while submitting an old body unconditionally makes the effect difficult to understand and encourages accidental reversal of other work.
If-Match: * is different from matching a particular revision. It establishes the existence condition described by the standard; it does not say that the resource remains the version the client read. A client that substitutes the wildcard whenever a version is inconvenient loses the stale-edit guarantee. RFC 9110, If-Match.
Creation needs a complementary decision. A caller creating a chosen identifier may want the operation to succeed only if no current representation exists. The standard defines If-None-Match: * for that condition, with different outcomes for retrieval and other methods. The application still needs to implement the condition atomically with creation. RFC 9110, If-None-Match.
Document the distinction in terms of intent: update the version I saw, create only if absent, or intentionally replace a newly reviewed state. Clear intent makes it easier to reject an inappropriate request than a loose requirement to “send some ETag header.”
Do not confuse cache validation with edit protection
A browser can use an ETag to avoid downloading an unchanged representation. That is a retrieval optimization. An editor can use an ETag to avoid replacing an unexpectedly changed representation. That is a write condition. Sharing a validator does not make the two flows interchangeable.
A 304 Not Modified response tells the client how to reuse its cached representation under the retrieval condition. It does not authorize a later write. The editor still has to submit its base validator with the state-changing request. RFC 9110 gives If-None-Match different response behavior for GET or HEAD than for other methods. RFC 9110, precondition ordering.
Freshness adds another distinction. A cached representation may be permitted for display under the cache policy even while the origin has newer data. Conditional editing can tolerate that stale display safely because the origin rejects a stale write. The user experience may nevertheless become frustrating if every editing session starts from an old cache entry.
A product can choose to refresh before opening an editor or after a conflict. That reduces avoidable conflicts but cannot replace the write condition: another writer may act immediately after the refresh. “We fetch right before saving” is a time-window reduction, not an atomic concurrency guarantee.
Personalized resources require careful cache boundaries. A conflict-recovery fetch should obtain a representation authorized for the current user. Publicly caching an error body containing another user’s current state could create an unrelated data exposure. Whether to embed current state in a conflict response is therefore an application contract decision, not a requirement of the precondition mechanism.
Also distinguish a stale client cache from a stale server replica. If the comparison happens against a replica that lags the authoritative write state, the condition can be evaluated incorrectly unless the final mutation enforces the expected version at the authority. The storage write boundary is where the correctness property must ultimately hold.
This distinction helps diagnose incidents. A high conflict rate may indicate old client snapshots and be a usability issue. A silently overwritten update indicates that acceptance was wrong and is a correctness issue. Treating both as “cache problems” can send the investigation in the wrong direction.
Recover intent with a three-way comparison
After Owen receives a failed precondition, the interface should preserve his edited name and fetch the current server state. It now has a base record with the old address and old name, a local draft with the old address and new name, and a current record with the new address and old name.
A three-way comparison asks which values changed relative to the base. In this simple example, Owen changed the name, and Maya changed the address. A proposed merged draft can preserve both changes. The user can review it and submit it with the validator associated with the current state. The merge remains a proposal until the next conditional write succeeds.
A field conflict arises when both writers changed the contact name differently. The interface should show the competing values and avoid choosing solely by timestamp. A later save can represent less informed work. Time order establishes which value committed first; it does not establish which value the user now intends.
Cross-field invariants can block an otherwise mechanical merge. If one writer changed country and another changed postal code, combining both values may create an invalid address. The application needs domain-aware validation after merging. A text diff alone cannot determine whether the resulting record makes sense.
Array editing is especially treacherous. If rows are identified only by position, deleting the first row can make every later row appear changed. Stable item identifiers permit a more meaningful comparison, but they do not decide how to merge reorderings or duplicate insertions. Define supported merge behavior around the product’s actual editing actions.
The user’s local work should survive navigation and transient failures according to the product’s persistence policy. For a long form, retaining the draft locally can be appropriate, but sensitive data may require different handling. Whatever storage choice is made, do not erase the draft because the server correctly prevented an overwrite.
A good conflict message states the result and the next action: the save did not apply because the record changed; review the newer values while keeping your draft. It should not say that the network failed or that the edit was saved partially when neither claim is true.
A retry needs an identity as well as a base
Suppose Maya’s request commits, but the response is lost. Retrying the same request with revision twelve may now fail because the resource has revision thirteen. That failure alone does not tell the client whether Maya’s first attempt succeeded or another writer changed the resource.
RFC 9110 allows a limited successful response when the origin can determine that the requested change already succeeded; a system must not infer that merely from similar current content. The practical product design still needs a way to resolve uncertain outcomes. RFC 9110, If-Match.
One proposed application mechanism assigns a unique mutation identity and records the result transactionally with the write. A retry bearing the same identity can recover that result under a documented contract. This is an application-level design, not a claim that ETags provide an idempotency-key protocol automatically.
Without such a mechanism, the client can fetch current state and compare it with the intended result, but matching values may remain ambiguous. A note appended twice, a payment triggered twice, or an increment applied twice cannot always be reconstructed from the current representation. Choose mutation semantics that make uncertain outcomes recoverable.
PATCH does not automatically solve this. A patch that sets a name differs from a patch that appends an item or increments a counter. The patch format and base assumptions determine whether resending is safe. RFC 5789 discusses conditional requests for patches that depend on a known base and addresses recovery when a response is lost. RFC 5789, PATCH method.
Avoid a retry loop that obtains the latest validator and resubmits the original stale replacement. That loop eventually wins by overwriting other work. It converts a correct conflict signal into permission to ignore the conflict. Automated rebasing should occur only when the application can establish the relevant intent and invariants.
A bounded retry policy can still improve reliability for a request known not to have reached the application. The important point is that transport recovery and semantic recovery are different decisions. An HTTP status, a timeout, and a committed mutation each carry different evidence about whether repeating an action is appropriate.
Include writers that never open an editor
The concurrency policy must cover scheduled imports, administrative tools, and internal services as well as browsers. Suppose a nightly integration reads customer data at midnight and uploads a complete replacement at six in the morning. A human edit during that interval is just as vulnerable as Owen’s stale form. If the integration bypasses revision checks through a privileged internal route, the public API’s protection does not cover the actual population of writers.
Inventory the paths that can change the representation and decide which intent each expresses. A reconciliation job may deliberately update one externally owned field while preserving locally owned fields. A bulk correction may need to recompute its proposed changes from the current state. Neither intent is accurately expressed by uploading an old complete snapshot. The repair should change the mutation model or enforce the same conditional boundary.
This also affects observability. A conflict report that names only interactive sessions can blame users for collisions caused by an import. Recording the writer type and operation identity can reveal that pattern without retaining the entire customer record. The condition remains consistent; recovery differs according to who owns the proposed change.
Test the race, the failure, and the recovery
A meaningful test starts two writes from the same accepted validator and forces them to overlap at the storage boundary. For incompatible replacements, exactly one should commit. The losing client should receive the documented failed-condition result. Inspect the committed representation to establish that it contains the winner’s complete intended change.
A second test checks bypasses. Send a write without the required condition, a weak validator, a wildcard, and a tag for another resource. The expected outcomes should follow the declared API semantics. These cases are more valuable than simply asserting that a response contains an ETag header.
A third test interrupts the response after a successful commit. Exercise the client’s uncertain-outcome recovery. Verify that repeated action does not duplicate a side effect and that the user can discover the result. The storage and notification paths both matter when the edit triggers external work.
For the interface, simulate a conflict while the user has a substantial draft. Confirm that the base and local changes survive, that current values are fetched under the correct authorization, and that accepting a merge sends the current base validator. A server can be correct while an interface loses all of the user’s work on every conflict.
Finally, measure conflicts with enough context to distinguish causes. Resource type, client version, editing duration, and outcome can identify an old integration or an overly broad concurrency boundary. Avoid logging full sensitive request bodies merely to explain a revision mismatch. The revision history and selected change metadata may be sufficient.
The release criterion is the same concrete outcome established at the beginning: committed work survives a stale replacement, and the person whose save was rejected can recover their intent. ETags are the protocol instrument. Atomic acceptance and thoughtful recovery are what make that instrument useful.
Sources
- RFC 9110: HTTP Semantics, representation validators and conditional requests.
- RFC 6585: Additional HTTP Status Codes, the meaning and caching restriction of 428.
- RFC 5789: PATCH Method for HTTP, patch base conditions and transport-failure recovery.