Idempotency-Key Implementation in REST APIs: Stopping Double Charges Without Missing the Second Request
Implementing idempotency keys in a REST API requires more than just deduplicating on key - it demands storing enough detail to reject mismatched retries and safely handle concurrent…

What is in this piece
When a request replay shares the same key, endpoint, and payload as the original, the stored response should be returned. If the fingerprint differs, the replay should fail. Concurrent retries must leave only one operation running, with a processing state and temporary response often handled by middleware. Stored records must also carry an expiration time.
Request Fingerprint and Record Shape
To replay a request correctly or reject an unrelated retry, the server needs to store the idempotency key in combination with a request fingerprint. The Go middleware package velmie/idempo fingerprints requests with method, URL, body hash, and optionally certain headers, blocking accidental key reuse with a different request 3,4.
A Rails implementation calculates a request signature from the URL, method, and body hash to identify unique requests tied to one idempotency key. This ensures that even retries with different payloads are not replayed 7.
A Spring Boot guide recommends records that include caller scope, endpoint, key, fingerprint, state, response status, and an expiration timestamp. Only upon a state change, typically from PENDING to either COMPLETED or FAILED, is the actual response stored to avoid replaying non-terminal states or states that might be workflow-specific 11,14.
Replacement fields: 2/2
What Happens on a Repeat
When a server receives a repeat request with a matching idempotency key, it consults the state. If the first request is no longer PENDING or PROCESSING, a stored final response is returned, not a fresh computation. velmie/idempo populates Result.Response on this check to enable direct response replay from the middleware 5,6.
If the first request is still running, only the owner may perform further operations. A non-owner sees Result.IsOwner as false, while the owner can proceed and commit. Ownership must be guarded carefully to avoid conflicting concurrent request handling. On failure, both velmie/idempo and the Rails implementation unlock the key to allow retries by the owner 5,6,7,8.
The Running-Request Case
Processing and committing an operation as its owner yields full control over the final response, state, and retention, but handling concurrent non-owners requires more nuance. The Spring Boot implementation shows a 409 Conflict with Retry-After and no service action, while the Rails implementation indicates a replay state without committing a response. More detail may be required on proofs, timeouts, and expiries for either approach to avoid non-owners from falling into a bottleneck 8,12.
Replacement fields: 0/0
Expiration and Retention
Expiration for idempotency records balances usability, storage efficiency, and security. Newer records cannot overhang earlier usage if constraints change. Stored responses, though often replayed from temporary caches, must ultimately expire once they can no longer be reset or recomputed fresh 10,13.
While practical heuristics offer one fallback for deployments lacking a strong specification, the Rails implementation still advocates defining an appropriate window based on the expected retry interval. The Spring Boot implementation offered a 24-hour TTL, though no primary-spec source specifies the correct TTL or expiration behavior across implementations and schemas 10,11,13,14.
What to Store, and What Not to Invent
When implementing idempotency keys in your API, several key pieces of data are required to reliably prevent double-charges, enforce single-semantics, and replay responses for matching retries:
- The idempotency key itself, as a key/index for retrieval 1,2,9
- The unique request fingerprint detailing endpoint and payload 1,2,3,4,5,11,12
- The processing state to differentiate in-flight requests from rejected, completed, or failed ones 5,6,7,8,11,12,14
- The final response to serve on replay from innumerable retry attempts 1,4,5,6,7,11,12,14
- The owner identity to allow retries in progress and prevent accidental duplicate processing 5,7,8
- The expiration time for old keys and immutable responses to prevent permanent state 10,13,14
- Failed requests as a separate state or response, or avoid or retry as appropriate, to satisfy client retry logic 11,12
Replacement fields: 0/0
Constructing a schema or memory-store view with this data avoids ambiguous replay, concurrent execution, unintended statefulness, and squandered computation. The idempotency state, once committed, can reify the request enough for any service to replay the outcome correctly on idempotent re-submissions.
Establishing an idempotent contract for your endpoints, while nontrivial, merits a synthesis of these components directly. Accounting for incremental states and replacing work with stored results, when applied consistently, sets the table for reaching single-request performance and consistency even under duplicated or absent requests.
- 01APIs & Protocols
The Hypertext Application Language
HAL is a simple format that gives a consistent and easy way to hyperlink between resources in your API.
- 02APIs & Protocols
What Poker Platforms Teach About Secure API Sessions
Poker platforms move real money. They fight fraud every day. If their sessions fail, chips and cash can vanish. APIs face the same risks. A weak session lets an attacker…
- 03APIs & Protocols
409 vs 422: When to Use Which Status Code for a Rejected Request
As a developer, you've likely hit a code review where two hardworking colleagues spar over which HTTP status code to return for a rejected request. Is it 409 (Conflict) or 422…
- 04APIs & Protocols
RFC 9457 Problem Details: Retiring Your Home-Grown Error JSON
When creating an error JSON, most engineers start by copying a format from a previous project. Over time, this casual practice leads to a proliferation of custom error shapes in every…

