Idempotency

Updated June 3, 2026
M
Magic Magnets Team
8 min read

In 2020, a user tried to pay for their order on a food delivery app. The request timed out. The app retried. The user got charged twice. Both orders arrived. This is exactly the kind of bug that idempotency is designed to prevent.

Idempotency is one of those concepts that sounds abstract until you've shipped a production bug without it — then it becomes something you think about every time you design a mutation.

What Idempotency Means

An operation is idempotent if performing it multiple times produces the same result as performing it once.

The simplest example is a light switch with a "turn off" button (not a toggle). Pressing it once turns the light off. Pressing it ten more times — same result. That's idempotency.

The counterexample is a "charge $10" operation. Run it once: $10 charged. Run it twice: $20 charged. Not idempotent.

In a mathematical sense: f(f(x)) = f(x) — applying the function repeatedly has no additional effect after the first application.

algobase.dev
Without idempotency, retries cause duplicate operations. The client sends a charge request — the server processes it and writes to the DB, but the response is lost in transit (network timeout). The client cannot tell whether the server processed it, so it retries. The server has no memory of the first request and charges again. The user gets charged twice. This exact bug has hit every major payments system at some point — and it is why retry logic, which every resilient system needs, depends on operations being idempotent.
1 / 1

Double-charge problem — timeout causes client retry, both requests processed

Why It Matters for Distributed Systems

Networks are unreliable. Any request can fail in ways that leave you uncertain whether the server processed it:

  • The request was sent but the network dropped the response (the server did process it)
  • The request never reached the server (the server did not process it)
  • The server processed it but crashed before sending a response

From the client's perspective, these three scenarios look identical: a timeout or connection error. The client has to decide: do I retry?

If you don't retry, you risk a bad user experience (the payment "failed" even though it went through). If you do retry blindly, you risk double-charging the user.

Idempotency is what makes retrying safe.

This is why retry logic — which every resilient distributed system needs — depends fundamentally on the underlying operations being idempotent.

Quiz Time

A client sends a payment request and receives a timeout. From the client's perspective, which of the following is true?

algobase.dev
Idempotency keys make retries safe. The client generates a UUID before sending and attaches it as Idempotency-Key: uuid-abc123. The Payment Service checks Redis before processing: on a cache miss (new key) it charges the DB and stores the result in Redis; on a cache hit (retry) it returns the stored result immediately — the DB is never touched again. The dashed DB path is skipped entirely on every retry. Stripe retains idempotency keys for 24 hours. The charge happens exactly once, regardless of how many times the network fails and the client retries.
1 / 1

Idempotency key solution — Redis lookup deduplicates on second request, returns cached result

Idempotency Keys

The standard solution for non-naturally-idempotent operations is an idempotency key.

The client generates a unique key (usually a UUID) for each logical operation and includes it in the request as an Idempotency-Key header. The server stores the key and its result after processing. If the same key arrives again, the server returns the stored result without reprocessing. The client can safely retry as many times as needed — the operation executes exactly once.

POST /v1/charges Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json { "amount": 2000, "currency": "usd", "source": "tok_visa" }

If this request times out and the client sends it again with the same idempotency key, Stripe looks up that key, finds the original charge result, and returns it — no second charge is created.

Implementation Considerations

Key storage: You need a fast key-value store (Redis works well) to check for duplicate keys before processing. The lookup must happen in the same transaction as the operation, or you risk race conditions where two concurrent retries both check the key, find nothing, and both proceed.

Key expiration: Keys don't live forever. Stripe retains idempotency keys for 24 hours. After that, the same key generates a new request. Set an expiry that makes sense for your retry windows.

Scope: Idempotency keys are typically scoped per API key or per user, not globally. user_123:key_abc is a valid approach.

Quiz Time

An idempotency key lookup must happen in the same transaction as the operation it guards. Why?

HTTP Methods and Idempotency

HTTP methods have defined idempotency semantics in the HTTP spec — knowing them helps you design better APIs.

MethodIdempotent?Safe?Notes
GETRead-only, no side effects
HEADSame as GET, no body
PUTReplaces the full resource — same result each time
DELETEDeleting something twice = it's still deleted
POSTCreates new resources — not naturally idempotent
PATCH*Depends on the operation

GET is trivially idempotent — reading data doesn't change state.

PUT is idempotent because it's a full replacement. PUT /users/123 with the same body always results in the same state for user 123, no matter how many times you call it.

DELETE is idempotent — once deleted, deleting again is a no-op (you'd return 404, but the system state is the same: the resource doesn't exist).

POST is the tricky one. POST /orders creates a new order each time. This is why you need idempotency keys for POST requests that represent operations you don't want to duplicate.

Quiz Time

Which of the following operations is NOT naturally idempotent?

PATCH depends on the operation:

PATCH /balance {"operation": "add", "amount": 10} ← not idempotent (relative update) PATCH /users/123 {"name": "Alice"} ← idempotent (absolute set)
Quiz Time

A PATCH request is always idempotent.

Real Example: Stripe's Idempotency Keys

Stripe's payment API is the textbook example of idempotency done right. Every mutating API call supports idempotency keys. Stripe's documentation explicitly tells you:

Results are saved for at least 24 hours, and Stripe returns the same result for any subsequent retries with the same key.

Their recommendation: generate the idempotency key on the server side (not the client/browser) before initiating payment. Store it alongside your own order record. If the user hits "pay" twice or if your backend retries on a timeout, the same key ensures the charge happens exactly once.

Stripe goes further — if you retry with the same key but different parameters (e.g., a different amount), they return a 400 Idempotency-Mismatch error. This catches bugs where the client is incorrectly reusing keys.

Quiz Time

Stripe returns a `400 Idempotency-Mismatch` error when the same idempotency key is reused with different request parameters. What problem does this behavior prevent?

Summary

Idempotency is what separates a naive API from one that's safe to operate at scale. Networks fail. Servers crash. Clients retry. If your mutations aren't idempotent, those retries become correctness bugs — double charges, duplicate records, inconsistent state. An idempotent operation produces the same result no matter how many times it runs. Idempotency keys make non-naturally-idempotent operations like payments safe to retry. GET, PUT, and DELETE are idempotent by design; POST is not. Stripe's implementation is the reference to study when designing payment flows.

Whenever you design an API endpoint that creates or modifies data, ask yourself: "What happens if this runs twice?" If the answer is "something bad", you need idempotency.

Data Formats

How helpful was this content?

Comments

0/2000

Sign in to join the discussion

Saved on this device only

Sign in to sync progress across devices