What Is Idempotency? (In APIs and HTTP)
Idempotency means doing an operation many times has the same effect as doing it once. Here is why retries make it essential in APIs, which HTTP methods are idempotent, and how idempotency keys stop a retried request from double-charging a card.
Definition
An operation is idempotent if performing it multiple times has the same effect as performing it once. Sending the same request twice leaves the system in the same state as sending it once. It is a promise about the effect on server state, not about identical response bodies.
The idea predates the web. In mathematics a function f is idempotent when f(f(x)) = f(x) — applying it again changes nothing. Absolute value is the classic example: abs(abs(-3)) is just abs(-3). MDN carries that straight into HTTP: an operation is idempotent if it 'can be applied multiple times without changing the result beyond the initial application.'
The concrete test is state, not the number of calls. Setting a user's plan to pro is idempotent — run it once or a hundred times and the plan is pro. Appending an item to a cart is not idempotent, because each call adds another item and the end state keeps changing. That single distinction is what makes an API endpoint safe to retry or dangerous to retry, and it is the reason the concept earns its own word.
Why it matters: networks are unreliable
Idempotency matters because of one unavoidable fact: the network can fail after the server did the work but before the response gets back. The client sees a timeout and cannot tell whether the request succeeded or vanished. Its only reasonable move is to retry — and retries are built into virtually every HTTP client, load balancer, and job queue. If the operation is idempotent, the retry is harmless. If it is not, the retry is a second charge, a duplicate order, a double-sent email.
HTTP methods and idempotency
RFC 9110, the HTTP semantics spec, is explicit about which methods carry the idempotency guarantee. GET, HEAD, PUT, DELETE, OPTIONS, and TRACE are idempotent by definition. POST is generally not — two identical POSTs to a collection usually create two resources. PATCH is left to the implementation: replacing a field with a fixed value is idempotent, but a patch that means 'increment this counter' is not.
| Feature | Method | Idempotent? | Safe (read-only)? |
|---|---|---|---|
| GET / HEAD | — | ✓ | ✓ |
| PUT | — | ✓ | — |
| DELETE | — | ✓ | — |
| POST | — | — | — |
| PATCH | — | Depends | — |
Notice the second column. Idempotent is not the same as safe. A safe method never changes server state at all — GET and HEAD only read. An idempotent method may change state, but repeating it lands on the same end state. DELETE proves the two are different: it is idempotent (deleting twice leaves the thing deleted) yet emphatically not safe (the first call destroys data). Keep the axes separate — 'safe' asks does it modify anything, 'idempotent' asks is repeating it harmless.
One honest caveat the spec is careful about: idempotency is about the effect on state, not about getting an identical response. A first DELETE may return 200 and a second may return 404 — different status codes (see HTTP status codes explained), same end state. The resource is gone either way, so the method is still idempotent.
Making non-idempotent operations idempotent
Most business-critical writes are POSTs — charge a card, place an order, send an invoice — so 'just use PUT' is not an answer. The job is to add idempotency on top of a non-idempotent operation. Here is the naive charge endpoint that double-charges on retry, next to the idempotency-key version that does not:
// NAIVE — a retry after a lost response charges the card twice
app.post('/charge', async (req, res) => {
const { amount, cardId } = req.body;
const charge = await gateway.charge(cardId, amount); // runs every call
await db.charges.insert(charge);
res.json(charge);
});
// IDEMPOTENT — client sends a unique key; server dedups on it
app.post('/charge', async (req, res) => {
const key = req.header('Idempotency-Key'); // e.g. a client UUID
const seen = await db.idempotency.get(key);
if (seen) return res.json(seen.response); // replay: return stored result
const charge = await gateway.charge(req.body.cardId, req.body.amount);
await db.idempotency.put(key, { response: charge });
res.json(charge);
});The idempotency key is a unique token the client generates (usually a UUID) and sends with the request. The server stores the result under that key; when the same key arrives again — because the client retried — it returns the stored response instead of re-charging. This is exactly the pattern Stripe and PayPal expose via an Idempotency-Key header. It requires server-side state: without somewhere to remember keys, there is no dedup, and that store is the real cost of the pattern.
Idempotency keys are the general tool, but there are lighter options when the shape of the data allows it:
- Natural unique constraints — dedup on a business id (a database
UNIQUEonorder_numbermakes a duplicate insert fail loudly instead of double-creating). - Upserts —
INSERT ... ON CONFLICT DO UPDATEcollapses 'create or update' into one repeatable operation. - Conditional requests —
ETagplusIf-Matchlets a write apply only if the resource has not changed since you read it, so a stale retry is rejected.
How this shows up in a BugMojo bug report
Double-submit and retry bugs are miserable to reproduce from a description, because the trigger is a timeline, not a single action: a request that succeeded, a lost response, and a retry that fired the write again. 'The customer was charged twice' tells you the symptom but not the sequence. What you need is the actual network trace — and that is what BugMojo captures alongside the session.
When a report comes in, the extension has already recorded the network panel, so you can see the two POST /charge calls with the same body and — critically — whether they carried the same Idempotency-Key or none at all. Pair that with the console (an unhandled promise rejection on the first attempt is a common trigger; see fixing uncaught-in-promise errors) and the duplicate request stops being a mystery. The captured request timeline is the proof that a retry, not a user, sent the second charge.
BugMojo captures the full network timeline with every bug report — so a retried, double-fired POST shows up as two real requests you can inspect, keys and all, instead of a symptom you have to guess at.
Install the extensionFrequently asked questions
Frequently asked questions
Sources
- Idempotent — Glossary definition of an operation with the same effect whether applied once or many times — MDN Web Docs (2026)
- RFC 9110: HTTP Semantics — §9.2.2 Idempotent Methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE) — IETF RFC 9110 (2022)
- HTTP request methods — per-method safe/idempotent/cacheable properties — MDN Web Docs (2026)
- Idempotence — the mathematical origin: f(f(x)) = f(x) — Wikipedia (2026)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

