Quick answer: Reliable JSON API idempotency requires one stable key per logical operation, payload matching, an atomic first-request claim, duplicate detection before side effects, durable response rules, a published retention window, and idempotent downstream processing. Missing one of these controls can turn a harmless timeout retry into two orders, charges, reservations, or messages.
Timeout handling is the most common reason a legitimate client repeats an ambiguous write. The JSON API timeout mistakes guide shows how to keep retries inside one deadline, propagate remaining time, and stop underlying work when the caller leaves.
The dangerous moment is uncertainty. A client posts an order, the server commits it, and the response disappears. Retrying is necessary, but executing twice is not. These seven mistakes show how to make retries predictable without pretending that one HTTP header creates exactly-once delivery everywhere.
For general response and error conventions, start with JSON in REST APIs. This guide focuses on repeated mutating requests and their side effects.
Use JSON Compare to review repeated sample request payloads. Matching JSON values alone does not prove that an operation is idempotent or that an order was processed only once.
1. Does every retry generate a new idempotency key?
Generate a key once for the logical operation and reuse that exact value for every retry. If a timeout handler, page refresh, queue redelivery, or SDK wrapper creates a new key, the server sees new work and may legitimately create another order. Persist the key beside the client's operation state.
This function is unsafe when the caller invokes it again after a timeout:
async function createOrder(body) {
return fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify(body)
});
}
The retry gets a different UUID. Generate the key when checkout begins, store it with that order attempt, and pass it into each network try. A browser refresh should reload the pending attempt instead of inventing a new identity.
Stripe's idempotent request documentation recommends a unique client-generated key and returns the saved result for later requests using that key. Shopify's current idempotent requests guide likewise describes keys as client-generated identifiers used to recognize duplicates.
Scope uniqueness deliberately, often by tenant, endpoint, and key. A sequential value can collide across accounts. A random UUID is a practical source, but randomness does not remove the need to save and reuse it.
2. Can one key be reused with a different JSON payload?
Bind every key to its intended operation and a stable fingerprint of relevant request data. If the same key arrives with a different amount, customer, currency, or items, return a documented conflict rather than replaying an unrelated response or executing changed work. Define which path, header, and normalized body values participate.
These requests must not share one result:
{ "customerId": "cus_42", "currency": "INR", "amount": 2500 }
{ "customerId": "cus_42", "currency": "INR", "amount": 3500 }
Hashing raw JSON bytes is fragile. Whitespace and object-member order can differ without changing data, while omitted defaults may change meaning. Validate first, build a canonical business representation, include endpoint and tenant scope, then calculate the fingerprint. Our JSON canonicalization checklist explains why equivalent data does not automatically produce identical bytes.
AWS EBS idempotency documentation returns a conflict when a client token is retried with changed parameters. Stripe also compares incoming parameters with those associated with the original key.
Do not store secrets in logs or idempotency tables merely for comparison. Save only required validated fields or a protected digest, and restrict stored responses because they may contain customer information.
3. Is the idempotency claim atomic under concurrency?
A read-then-insert check fails when two identical requests arrive together: both can observe no record and both can start processing. Claim the scoped key atomically with a unique database constraint or equivalent coordination mechanism, then define whether the losing request waits, polls, or receives an in-progress response without repeating side effects.
This race is common:
| Time | Request A | Request B |
|---|---|---|
| T1 | Finds no key | Finds no key |
| T2 | Charges provider | Charges provider |
| T3 | Inserts key | Unique insert fails |
The constraint protected the final row, not the charge. Insert an in_progress claim before external work, in the same transaction as local state when possible. The losing request reads the existing record instead of proceeding.
Handle abandoned claims with timestamps and an owner or fencing token. A simple timeout must not let a second worker take over while the first is still active.
The IETF worked on an Idempotency-Key header draft covering duplicate, mismatch, and concurrent requests. However, the datatracker labels the latest draft expired, so cite it only as work in progress, not as a current standard. Your service must document its actual contract.
4. Do duplicate checks happen after business side effects?
Check and claim the idempotency key before charging, reserving inventory, publishing a message, or calling another service. A uniqueness failure at the final order insert is too late because duplicate requests may already have repeated irreversible work. Arrange the handler so idempotency is the first state-changing gate after authentication and validation.
A practical order is:
- Authenticate and validate input.
- Normalize the operation and calculate its fingerprint.
- Atomically claim the key.
- Return a completed stored result when present.
- Reject a fingerprint mismatch.
- Apply the documented behavior for in-progress work.
- Let only the claim owner perform business effects.
- Persist final status and the response reference.
A recent public order-service bug report described duplicate requests reserving stock before a unique-key insert failed. That is useful qualitative evidence of the failure pattern, not a statistic about prevalence.
Effects triggered after a database commit need additional coordination. A transactional outbox can save an event with the order, then let a worker publish it. Recipients should still deduplicate because message brokers may redeliver after uncertain acknowledgments.
5. Does the server remember only successful responses?
Define how validation failures, business declines, server errors, and in-progress attempts interact with a key. Saving only successes can re-execute work when the original operation committed but response generation failed. Persist enough durable state to distinguish never started, currently running, and completed with a specific result or resource reference.
Provider behavior differs. Stripe documents saving the first result after endpoint execution begins, including a 500 result, while some failures before execution are not saved. This lets corrected pre-execution requests be attempted again while completed execution remains stable.
Your contract should answer:
- Is the original HTTP status replayed?
- Is the response body stored or reconstructed?
- What happens if serialization fails after commit?
- Can a validation failure reuse the key after correction?
- How are concurrent requests represented?
- Does a declined business operation count as completed?
Avoid retaining sensitive response bodies unnecessarily. An idempotency record can reference the durable order and reconstruct an authorized response. Recheck the caller's permissions before returning it; knowing another tenant's key must not reveal their data.
Use structured errors for mismatch and in-progress states. The JSON API error response guide covers machine-readable codes and safe details.
6. Is key retention shorter than the actual retry window?
Retain idempotency records long enough to cover automatic retries, delayed queues, mobile reconnects, webhook redelivery, and manual recovery. If the server deletes a key before a late retry arrives, it can interpret old work as new. Publish the retention period and warn clients not to assume protection after expiration.
Retention differs across providers and operations. Stripe says keys can be removed after at least 24 hours. AWS documents 24 hours for some ECS client tokens, while other AWS services choose different periods. These are product-specific policies, not universal recommendations.
Choose a window from evidence:
| Input | Question |
|---|---|
| Client retry policy | What is the longest automatic backoff? |
| Queue redelivery | Can messages return days later? |
| Offline clients | How long can a device retain uncertain work? |
| Manual recovery | Can support replay after an incident? |
| Business impact | What does one duplicate transaction cost? |
| Stored data | Can responses be minimized or encrypted? |
If a merchant order number must stay unique longer, enforce that as a separate domain constraint. Idempotency retention is a retry mechanism, not a replacement for durable business invariants.
7. Can downstream services still repeat the same work?
An idempotent HTTP endpoint does not automatically protect database writes, queues, webhooks, email, or payment-provider calls. Carry a stable operation identity through the workflow, enforce domain uniqueness, use an outbox where appropriate, and make each consumer record processed messages atomically with its own state change before acknowledgment.
Suppose /orders returns the original order on retry, but a worker publishes OrderCreated twice after crashing between send and acknowledgment. Inventory may still be reserved twice. The inventory consumer needs its own deduplication key, such as an immutable event ID, and an atomic link between recording that ID and changing stock.
This is why exactly-once claims need caution. Idempotence means repeated application has no further effect beyond the first. Distributed systems still contain uncertain network outcomes and independently committed components. Idempotent handling makes repetition safe; it does not erase those boundaries.
Test the complete path:
- Send two concurrent requests with the same key and body.
- Retry after commit but before the response reaches the client.
- Reuse the key with a changed amount and expect a conflict.
- Crash a worker after an external call but before acknowledgment.
- Redeliver one event to every consumer.
- Retry immediately before and after key expiration.
Assert order count, payment count, stock movement, event IDs, and returned response—not only the API status. The target is one intended business effect under the retry conditions you document.
These seven checks form one idempotency review: reuse the key, bind the request, claim atomically, gate side effects, persist outcomes, retain records long enough, and deduplicate downstream work. When each boundary has an executable test, retrying uncertain JSON requests becomes routine instead of financially dangerous.
For incoming deliveries, these six JSON webhook mistakes explain durable acceptance, atomic event claims, and recovery after worker crashes.