Why API Updates Overwrite Other Changes: ETag, If-Match and Concurrent-Write Acceptance Tests

When people and agents modify the same object, valid parameters, successful requests and even idempotency do not prevent stale content from overwriting newer changes. A constructed order example explains conditional updates with ETag and If-Match, atomic server-side checks, conflict recovery and concurrency tests, while separating object versions from cross-object constraints and unknown write outcomes.

One person changes an order's contact. Another changes its delivery date using an earlier copy, and the contact reverts. Both API calls succeed, yet an update is lost. Preventing this requires the write endpoint to check the state on which the modification was based. The following design is intended for teams controlling their API and data-write logic. Examples are constructed; no experiment on a real system is claimed.

Make the original state an explicit write precondition

Tool contracts and recovery for reliable multi-agent execution require checking object state before acting. Here we turn that principle into a narrower question: after a caller reads revision seven of an order, is its proposed update still eligible to commit? The issue is not whether two requests are identical, but whether two different intentions rely on evidence that has become stale.

Suppose A and B both read an order with Zhang as the contact and Tuesday as the delivery date. A changes the contact to Li and commits revision eight. B then submits a complete object containing the old contact, Zhang, and Wednesday delivery. If the server overwrites by order identifier alone, A's change disappears. Asking B to read once more immediately before submission is insufficient: someone can still write between that read and B's update.

Our conditional-update contract obtains the server's ETag when reading the object and sends that tag unchanged in If-Match when modifying it. RFC 9110 uses this precondition to prevent concurrent overwrites and requires evaluation before the corresponding method is performed.[1] The essential implementation issue is making the decision inseparable from the actual update, not merely adding a header at a gateway.

Make the tag cover the state that needs protection

An ETag is an opaque server-provided value; clients should not calculate or increment it themselves. Our illustrative quoted tag is "order-g3-v7"; real APIs may generate tags differently. If-Match uses strong comparison, so a weak tag with a W/ prefix cannot match for this purpose.[3] If the read API supplies only weak tags, establish a suitable conditional-write contract rather than removing the prefix and pretending the tag is strong.

In this proposed design, include object generation and revision in the tag and require every relevant write to advance the revision. Deleting and recreating an object must not reuse its old generation, or an old plan could mistake the replacement for the original. If the application needs to detect a value changing away and then back, use a revision advanced on every change rather than merely comparing a few current business fields.

Document the protected scope. If an order response includes contact information joined live from a customer table while its revision tracks only the order table, customer changes can bypass protection. Include relevant dependencies in the version and consistency checks, or split resources and explicitly limit this interface to the order itself. Cross-table and multi-object constraints need additional transaction design; one ETag is not a lock over all associated business state.

Whole-order versions are simple but can conflict on unrelated edits. Splitting contact and delivery settings may reduce conflicts while undermining rules that require them to change together. Choose granularity around business invariants rather than minimizing the conflict count at any cost. With multiple languages or representations, align the returned tag with the representation scope checked by the write endpoint. Never substitute a tag from another resource.

Compare and update atomically at the write endpoint

One implementable single-database design authenticates the caller and establishes tenant and object scope, then checks expected generation, revision and business conditions in the same transaction that updates fields and advances the revision. A conditional update can include tenant, object, generation and expected revision in its predicate. Only an actual change to one target record counts as a successful conditional write. Validate the SQL and isolation requirements against the database engine in use.

An appropriate row lock can alternatively protect reading, comparison and updating within one transaction. Do not compare the version in an ordinary query and then update by object identifier alone. Both requests may pass the first check and subsequently overwrite each other. The HTTP condition is the protocol entry point; storage-level handling of competition determines whether protection holds.

When the conditional update fails, distinguish absence, permission denial and version conflict according to the API contract without revealing another tenant's objects. Do not treat every zero-row outcome as a network fault and retry automatically. An ordinary stale-version conflict returns 412. Our proposed interface returns 428 for a write missing its required condition. RFC 6585 defines 428, but adoption is not mandatory across all services, so clients must follow the actual contract.[2]

Place events and external actions behind the right boundary. A rejected request must not first send a notification or deduct inventory. Local events associated with a successful change can be committed with the business record and dispatched later through a controlled process. External services do not automatically join that transaction. Administrative interfaces, bulk imports and scheduled jobs that bypass revision advancement can also invalidate the protection.

Reconsider the change after a conflict; do not just fetch a new tag

In the order example, after A successfully commits using revision seven's tag, B's request carrying that same old tag should be rejected. B must retain the original snapshot, its desired delivery change and the newly read current object. Comparing all three establishes whether the original intention can be fulfilled while preserving Li as the new contact.

If B merely fetches revision eight's tag and resubmits the old complete object containing Zhang, its next request may legitimately overwrite A's change. Conditional checking has not failed: the client has presented an old decision as though it were based on new state. Show the operator the conflicting differences or have the agent recompute affected conditions. Material changes requiring approval should remain bound to the new concrete content.

Define automatic merge boundaries in advance. For example, a delivery-note-only change may produce a new request containing only the approved modification if current state still permits editing. If the order has already shipped, the original delivery-change intention may no longer be allowed. Partial updates are not inherently safe: a single-field change can depend on another field's old value. Different fields and different authors do not imply freedom from business conflict.

Our acceptance design uses strict rejection when the condition fails. RFC 9110 also permits success when the server can determine that the requested change was already successfully applied; clients cannot assume that exception without supporting evidence.[1] For a third-party API, establish its conflict and duplicate-submission semantics instead of treating this example policy as universal implementation behavior.

Separate idempotency, version conflicts and unknown outcomes

A version tag asks whether current state still satisfies a precondition. An idempotency identifier asks whether this particular operation has already been processed. We recommend recording them separately rather than using one field for both. A and B may have different idempotency identifiers while carrying the same stale version. Deduplication will not prevent their changes from overwriting each other.

If A's write commits but its success response is lost, retrying with the old version may yield a conflict. That conflict alone does not prove that the first write failed to occur. Reconcile through a stable operation identifier, server receipt or status query. Even matching current field values may not prove that A's operation produced them. Audit attribution requires a record connecting the operation and committed revision.

When an API supports both idempotency records and conditional updates, specify how a completed operation with the same key and parameters returns its original receipt, and how a new operation checks the current version. Document retention, query consistency and handling of unknown results. If the outcome cannot be established, retain a pending-reconciliation state instead of creating a new operation identifier to force another execution.

An ETag is not an authorization credential. Knowing a tag grants no cross-tenant write permission; identity and current business rules still need checking when the version matches. Conditional updates also do not automatically enforce total inventory or limits spanning multiple orders. The transaction or service responsible for those business facts must validate such constraints.

Test protection with concurrent requests and failure scenarios

First, use a synchronization barrier so two distinct operations read the same version and then submit mutually exclusive changes together. Under this test contract, exactly one actual change should commit and the other should receive a version conflict. Inspect the final object, revision and event records rather than counting HTTP statuses alone. Reverse arrival order and repeat competition to avoid a sequential script passing simply because it never exposed a race.

Second, test entry conditions. Reject missing conditions according to this API's contract; stale and weak tags must not authorize the write. The wrong tenant must not update an object even with the same tag. After deletion and recreation, requests using the old generation should fail. Modify the object through administrative and import paths too, confirming that these also invalidate old tags.

Third, test recovery. Break the connection after commit but before response delivery and confirm that the client reconciles the original operation rather than generating a new intention. Simulate a faulty client that blindly refreshes the tag after a conflict and check that business logic blocks stale decisions or requires renewed confirmation. Verify that failed requests caused no premature business side effects and that late requests cannot restore a cancelled object.

Finally, track conflict proportion, manual merge time and reconciliation cost. Frequent conflicts may indicate an overly broad resource, long editing windows or continuously competing automation. Disabling protection is not the first remedy. Limit acceptance claims to tested APIs, write paths and business rules. This article supplies implementation and testing methods; it does not claim that any particular system has passed them.

References

  1. [1] IETF RFC 9110 — HTTP Semantics,§8.8、§13.1.1 (June 2022; accessed November 1, 2026)
  2. [2] IETF RFC 6585 — Additional HTTP Status Codes,§3 (April 2012; accessed November 1, 2026)
  3. [3] MDN — If-Match header (accessed November 1, 2026)
Back to insights
鲁ICP备2024109755号-2
Drag to move. Right-click, touch and hold, or press Shift+F10 to choose a corner.