What Is Idempotency in APIs?
Idempotency sounds intimidating but describes a simple, practical idea: making the same request more than once should have the same effect as making it exactly once. In API design, this property lets clients safely retry after a timeout without accidentally charging a card twice or creating duplicate records.
What idempotency actually means
An operation is idempotent if performing it multiple times leaves the system in the same state as performing it once. The result the client sees may be returned repeatedly, but no additional side effects accumulate.
A classic non-API example: pressing the “floor 5” button in an elevator. Pressing it once or five times produces the same outcome — the elevator goes to floor 5. Compare that to “add one to a counter,” which is not idempotent: every call changes the state.
It helps to separate idempotency from a related idea, safety. A safe method causes no state change at all (a pure read). An idempotent method may change state, but repeating it does not change it further. Every safe method is idempotent, but not every idempotent method is safe.
Which HTTP methods are idempotent
The HTTP specification defines idempotency at the method level. These are the conventions you should design around, summarized below. You can review the full set on the HTTP Methods Reference.
| Method | Safe | Idempotent |
|---|---|---|
| GET | Yes | Yes |
| HEAD | Yes | Yes |
| OPTIONS | Yes | Yes |
| PUT | No | Yes |
| DELETE | No | Yes |
| POST | No | No |
| PATCH | No | No |
PUT is idempotent because it replaces a resource with a complete representation: sending the same body twice results in the same final state. DELETE is idempotent because once a resource is gone, deleting it again leaves it gone — even if the second call returns 404 instead of 200, the state is unchanged. POST is not idempotent by default because it typically creates a new resource each time. PATCH is not guaranteed idempotent because a partial update can depend on current state (for example, “increment by one”).
Why idempotency matters
Networks are unreliable. A client sends a request, the server processes it, and the response is lost on the way back. The client now faces a dilemma: did the operation succeed or not? If it retries a non-idempotent operation, it risks a duplicate charge, a double-shipped order, or two identical database rows.
Idempotency turns “retry” from a dangerous gamble into a safe default. This is why robust client libraries automatically retry idempotent methods on connection errors and 5xx responses, and why payment APIs lean on idempotency for create operations. It also pairs naturally with rate-limiting and backoff strategies, where retries are expected behavior.
How to make POST idempotent with idempotency keys
Since creating resources via POST is inherently not idempotent, the established pattern is to let the client supply an idempotency key — a unique value, usually sent in a header such as Idempotency-Key. The server records the key with the result and replays that result if the same key arrives again.
A reliable implementation follows these steps:
- The client generates a unique key per logical operation (not per HTTP attempt) and sends it with the request.
- The server checks storage for that key. If absent, it processes the request, persists the key together with the response, and returns it.
- If the key already exists and the operation completed, the server returns the stored response without re-executing the work.
- If the key exists but is still in progress, the server returns a conflict (commonly
409) or asks the client to wait, so concurrent retries don't double-process.
A minimal sketch in pseudo-JavaScript:
const key = req.headers["idempotency-key"];
const existing = await store.get(key);
if (existing) return res.json(existing.body);
const result = await chargeCard(req.body);
await store.set(key, { body: result }); // persist atomically
return res.json(result);
Generating good idempotency keys
Keys must be unique per operation and stable across retries. A version-4 UUID is the common choice; generate one with the UUID Generator. If you want keys that sort by creation time for easier debugging, a ULID is a strong alternative. Create the key once on the client and reuse it for every retry of that same request — generating a fresh key per attempt defeats the entire mechanism.
Common mistakes to avoid
Even teams that understand the concept trip over the details:
- Treating PATCH as idempotent. A JSON Merge Patch that sets a fixed value can be idempotent, but operations like “append to a list” or “increment” are not. Document which behavior your endpoint guarantees.
- Generating a new key on each retry. The client must keep one key for the whole logical request, persisting it locally if needed across process restarts.
- Not making the store write atomic. If two retries race, both may pass the “does the key exist?” check before either writes. Use a unique constraint or an atomic insert so exactly one wins.
- Returning 404 from DELETE as if it were an error. A repeated DELETE that finds nothing is still a successful idempotent outcome; treat it accordingly rather than failing the client's retry loop.
- Expiring keys too aggressively. A key that is purged before a slow client retries reopens the duplicate-side-effect window. Many APIs keep keys for 24 hours or more.
- Ignoring response replay. Returning a different status or body for a replayed key confuses clients; store and replay the original response faithfully, often signaling it with a status header.
Where idempotency shows up beyond REST
The principle extends well past plain HTTP verbs. Webhook delivery is the canonical example: providers retry until they receive a success acknowledgment, so consumers must deduplicate by the event ID to avoid processing the same event twice. Message queues with at-least-once delivery require the same discipline — a consumer must be safe to run a message through more than once.
If you are still mapping out how your service exposes operations, our overview of what an API is covers the request/response basics that idempotency builds on. When you respond to retries, choose status codes deliberately; the HTTP Status Code Reference helps you distinguish a true conflict (409) from a successful replay (200).
A practical checklist
Before shipping an endpoint, confirm the following:
- GET, HEAD, PUT, and DELETE behave idempotently with no extra side effects on repeat calls.
- Any create-style POST accepts and honors an idempotency key, stored atomically and kept long enough to cover realistic retry windows.
- Replayed requests return the original response, documented in the contract.
- Consumers of webhooks and queues deduplicate on a stable event identifier.
Get these right and “just retry it” becomes a safe instinct rather than a source of duplicate charges and corrupted data.
Frequently Asked Questions
A safe method causes no state change at all, such as a GET that only reads data. An idempotent method may change state, but repeating it produces no additional change beyond the first call. Every safe method is idempotent, but PUT and DELETE are idempotent without being safe because they modify the server.
By default POST is not idempotent, since it usually creates a new resource on each call. You can make it effectively idempotent by having the client send an idempotency key (commonly an Idempotency-Key header). The server stores the key with the result and replays that stored response if the same key arrives again, so retries don't create duplicates.
Idempotency is about the resulting state, not the status code. After the first DELETE, the resource is gone; deleting it again leaves it gone. The second call may return 404 instead of 200, but the system state is identical, which satisfies idempotency. Clients should treat that repeated 404 as a successful retry, not an error.
Store them long enough to cover realistic retry windows, including slow clients and delayed background jobs. Many APIs keep keys for at least 24 hours. Expiring keys too early reopens the window for duplicate side effects, so balance storage cost against the longest plausible gap between an original request and its retry.
Not necessarily. PATCH is idempotent only if the partial update sets fixed, absolute values. Operations that depend on current state, such as incrementing a counter or appending to a list, are not idempotent because each call changes the result. Document clearly which behavior your PATCH endpoint guarantees so clients know whether retries are safe.