#TypeScript#DevOps

HTTP Idempotency Key Correctness: Payload Binding, Concurrent Acceptance Points, and Unknown-Outcome Recovery

Reusing an Idempotency-Key does not mean the requests are identical, and caching one response cannot prevent concurrent double writes. This article defines a contract for operation identity, canonical payload fingerprints, atomic reservation, result replay, and reconciliation of unknown side effects.

Key Points

  • Topic Focus: TypeScript、DevOps
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

Adding an Idempotency-Key to POST only expresses the client's intent to replay the same operation; it cannot prove that two requests are actually identical. If the server caches responses by key alone, an attacker or buggy SDK could first use key K to create an order for amount 10, then submit amount 100 using the same K. If the later request silently receives the old response, the caller will mistakenly believe the new parameters took effect. If two first-time requests pass the duplicate check concurrently, they can also produce two real writes. REST API Versioning addresses the evolution of API shapes, while MCP Timeouts, Retries, and Idempotency discusses recovery strategies for tool calls. This article focuses on the atomic acceptance contract of HTTP write endpoints themselves.

First, Fix the Operation Identity and Fingerprint Rules

Publish an immutable http.idempotency.v1 contract. The rev-07 document for Idempotency-Key is an expired Internet-Draft, not an RFC; the local contract is the actual specification. The deduplication scope must include at least the authenticated tenant, the principal or calling application, the route's business operation, the API major revision, and the client key. The request body must not be allowed to self-declare the tenant, and hits must never be shared across users. The key must be a high-entropy opaque value that contains no email address or order details, with limits on its length, character set, and the number of active keys per subject. API Rate-Limiting Strategies still handle abusive traffic; the idempotency table is not an unlimited-capacity rate limiter.

The requestFingerprint binds every input that can change the business effect: normalized path parameters, selected query parameters, the media type, business header versions, and the body. A protocol that requires raw bytes to be identical can hash those bytes directly. If the contract permits semantically equivalent JSON, first reject duplicate member names, non-finite numbers, and fields outside the schema, then canonicalize according to a fixed schema revision. RFC 8785 JCS number serialization reduces 10.0 and 10 to the same representation. If lexical differences such as an amount's scale have business meaning, the schema must use a fixed-point integer plus scale, a decimal string, or typed canonicalization; it cannot apply JCS number serialization directly. Arrays and null must likewise be transformed only according to an explicit contract.

text
scope = H(tenant, principal, operation, apiRevision, idempotencyKey)
fingerprint = H(method, canonicalTarget, mediaType,
                schemaRevision, canonicalPayload)

Only the same scope with the same fingerprint is a replay. The same scope with a different fingerprint is a PAYLOAD_CONFLICT, which must return a stable conflict error without exposing the original request body. The server should echo the protocol version and operation ID it used, and must not use the client-provided key directly as a business resource ID.

Use a Unique Record to Define the Acceptance Linearization Point

The idempotency record moves through RESERVED -> EXECUTING -> SUCCEEDED | FAILED_FINAL | UNKNOWN, with a separate REJECTED state that produces no effect. In a database transaction, the first request inserts a record under the unique scope and stores the fingerprint, contract version, operation ID, ownerGeneration, and recovery lease. After a unique-constraint conflict, it locks and reads the existing record. A worker may claim or take over only that same operation through CAS. Lease expiration transfers recovery ownership only; it does not authorize creating a second effect. Optimistic vs. Pessimistic Locking explains CAS and row locks. The key point is that "check for absence" and "reserve" cannot be split into two unprotected steps.

Execute the business transaction only after the reservation succeeds. If the effect and idempotency table share a database, commit the business write, terminal state, and transactional outbox together. The recoverer first reads the outcome of that transaction; only if it was not committed may a higher owner generation continue. The response snapshot stores only approved status codes, media types, resource locations, business versions, and the body or an encrypted digest of the body. It excludes Date, trace IDs, one-time tokens, and connection-specific headers. Later identical requests in SUCCEEDED/FAILED_FINAL return the same business result. Requests in RESERVED/EXECUTING return a processing status or result-query link and must not start another execution.

When a downstream effect cannot share a transaction with the local record, the service must register an effect attempt bound to the owner generation and a slot for the provider handle before sending, then pass a stable downstream idempotency key all the way through. If the process crashes at the sending boundary, the state remains UNKNOWN. The recoverer may only query by handle, replay with the original key, or send the operation for manual reconciliation. It must not change the key or claim that the effect did not occur merely because the lease expired. Background Job Queues can run recovery work, but at-least-once delivery does not mean exactly one business effect.

The key's retention period must cover the client's maximum retry window, queue backlogs, downstream reconciliation, and backup restoration. On expiration, retain a tombstone containing the scope and fingerprint. If the business cannot prove that the old effect can no longer appear, reject reuse of that scope instead of deleting the record and permitting a second charge. Cancellation only blocks requests that have not crossed the acceptance point. A committed operation must return its current result or enter an explicit compensation workflow.

Validate with Races and Lost Responses

The fault suite concurrently sends 100 identical payloads using the same key, then mixes in one different payload. Kill the process after reservation, before the business commit, after the commit but before the response, and after downstream success. Also induce transaction deadlocks, connection-pool timeouts, replays by old clients across API revisions, changes to the authenticated principal, changes in JSON member order, duplicate fields, and races at tombstone expiration. Assert that identical requests produce only one operation ID, one business row, and one outbox effect; every different fingerprint produces a conflict; and an UNKNOWN operation is not executed under a higher generation until reconciliation is complete.

The launch gate requires zero duplicate logical effects across 100,000 concurrent replays, zero false hits for payload conflicts, a 100% digest match rate for replayed terminal responses, fewer than 0.01% of UNKNOWN operations exceeding the recovery SLO, and a P99 reservation transaction that consumes no more than 5% of the endpoint's latency budget. Start in shadow mode, then ramp up by operation. During rollback, first increment the writer epoch, stop accepting new operations, and drain or isolate pending operations. Old and new writers must share the same idempotency registry. If the old implementation cannot consume that registry, it may fall back only to a read-only or manual path. It must not restore the old write entry point and let client retries bypass the new table. Always retain the new table and outbox for reconciliation.

Further Reading

Related

Related Content