You order a bag of coffee. The checkout spins, pauses, and announces that something went wrong. You press “Try again.” A moment later, two confirmation emails arrive. Congratulations: your connection recovered, and your kitchen is now better supplied than intended.

The first request might have succeeded. Its reply simply never reached you.
This is where API idempotency earns its awkward name: it lets an application repeat an operation without repeating its intended effect. Behind a trustworthy retry button is a system that can recognize, “We have already dealt with this particular request.”
A timeout is a missing answer, not a verdict
An application talks to a service through an application programming interface (API). Sending a request and receiving its response are separate events. Trouble between them leaves an uncomfortable gap in the client’s knowledge.
After a timeout, the request might never have arrived. It might still be running. Or it might have completed successfully while the response disappeared on the way back.
Here is a fictional checkout without duplicate protection:
| Step | What the customer sees | What the server does |
|---|---|---|
| First request | A spinning checkout button | Creates order A |
| Response fails to arrive | “Something went wrong” | Keeps order A |
| Customer retries | Another spinning button | Treats the request as new and creates order B |
The network can lose the receipt without undoing the purchase. PayPal documents exactly this class of problem: a payment capture can complete even though the caller receives a timeout. Its supported APIs use a request identifier to recognize a subsequent retry.
What does idempotent actually mean?
Imagine a control labelled “Set the thermostat to 20°C.” Press it once or five times: the requested setting is still 20°C. Now imagine “Increase the temperature by 1°C.” Five presses request five increases.
That is the useful distinction. Idempotency concerns the intended effect of repeating an operation, rather than whether every detail of its response stays identical.
HTTP builds this distinction into method semantics:
| Method | Idempotent by HTTP semantics? | Practical meaning |
|---|---|---|
| GET | Yes; also safe | Retrieve a representation rather than request a business change |
| PUT | Yes | Set the target resource to the supplied state |
| DELETE | Yes | Remove the target resource’s association |
| POST | Not inherently | An operation such as creating an order needs its own retry contract |
A repeated DELETE might receive a different status because the resource is already gone. That does not defeat idempotency. Servers can also log each attempt separately. The promise concerns the requested effect, and implementations must honor the method’s semantics. RFC 9110, Section 9.2.2 defines the distinction.
The idempotency key: a ticket for one intention
For an order-creation API, the client can supply an idempotency key: a unique identifier for one logical operation. The following is an illustrative request sketch, not a working endpoint or a universally supported header:
POST /orders
Idempotency-Key: 79b591dc-4013-47de-9f42-f98ba40256b3
Content-Type: application/json
{"sku":"coffee-42","quantity":1}
If that operation needs a retry, the client sends the same key and matching request parameters. A genuinely new purchase gets a new key, even when the customer orders the same coffee again.
This is why comparing request bodies is insufficient: two identical shopping baskets can represent two intentional purchases. The identifier communicates intent. AWS explains this reasoning in its guide to making retries safe with idempotent APIs.
Generate and retain the key before the first attempt. Generating a fresh one every time a timeout handler runs tells the service that every retry is a different operation—the opposite of the intended protection.
The difficult part lives on the server
Writing a key into a header is easy. Making its promise survive simultaneous requests and crashes takes more work.
A typical implementation keeps a durable record containing the key, its scope, a request fingerprint, a processing state, and the result or a reference to it. A duplicate that arrives during processing must not start a second independent execution. Once the operation completes, the service can return the recorded outcome according to its API contract. AWS Powertools provides a concrete example with in-progress and completed records.
The dangerous gap is between “create the order” and “record that this key created it.” A crash there can leave the next attempt unaware of the first. Where both changes live in one database, a transaction and an appropriate uniqueness constraint can make them commit together.
An external payment service or email sender is outside that database transaction. Its effects need their own duplicate protection or a recovery and reconciliation design. Saving a local key alone does not make the whole chain happen exactly once.
Read the contract: the rules differ
There is no guarantee that an arbitrary server will recognize Idempotency-Key merely because a client sends it.
Stripe’s API v1 documents that, once endpoint execution begins, it records the first response’s status and body for a key. A retry can therefore return the same 500 error. It also compares request parameters and allows keys to be removed after they are at least 24 hours old; reusing a pruned key starts a new request. These are specific Stripe API v1 rules, not universal properties of idempotency.
PayPal uses PayPal-Request-Id on supported APIs. Its documentation explicitly says support and retention depend on the API, and a retry returns the previous request’s latest status. Check the endpoint’s documented behavior rather than borrowing another provider’s assumptions.
In particular, an expired deduplication record can turn a very late retry into a new operation. When the outcome remains uncertain, use the service’s documented status lookup or reconciliation process before creating a fresh request.
Safe retries still need good manners
Idempotency protects an operation’s effect. It does not make repeated requests free: they still consume connections, processing time, and storage access.
A useful retry policy should:
- Retry only failures that the API contract identifies as retryable; repeatedly submitting unchanged invalid input will not fix it.
- Limit attempts and total waiting time.
- Use backoff with jitter, spreading retries over time instead of making every client retry together.
- Account for retries already performed by an SDK or another application layer.
AWS’s guide to timeouts, retries, and backoff with jitter explains why enthusiastic retries can worsen an overloaded service.
Disabling the checkout button is still sensible interface design. It reduces accidental clicks, but it cannot coordinate a restarted app, a background job, and an SDK that retries a request. The server has to recognize the operation too.
One question to ask before adding a retry
Ask: “If the first attempt already worked, what happens when this one arrives?”
For a well-designed order API, the answer should be another opportunity to learn about the same order. For an unprotected one, it might be another bag of coffee.
Reliable software needs a way to keep trying without accidentally asking for more.