Sign in
← Application patterns
11 min · medium · Lesson 5 of 5

Idempotency in APIs

Why a repeated request shouldn't create a duplicate: idempotent methods, the idempotency key, and safe retries.

Why this matters

A client sends POST /orders, the network blips, the response never arrives. The client doesn't know whether the order was created or not, and retries the request. If the server creates a new order on every POST, the retry makes a second one. This is exactly why the previous lesson on retries forbade retrying POST blindly. A timeout on the client cuts off waiting for a response, not the server's work: the order may already exist by then.

The fix for this is called idempotency. An idempotent operation gives the same result no matter how many times it's repeated. One order stays one order, whether the request was retried or not.

Which methods are idempotent on their own

The HTTP standard (RFC 9110) calls GET, HEAD, PUT, DELETE, OPTIONS, and TRACE idempotent. Read twice, replace a resource twice, delete twice: the state on the server ends up the same as after one call.

Idempotency is about the state on the server, not the response. A second DELETE of the same order can return 404, because there's nothing left to delete. The order is still deleted exactly once, and that's fine.

POST is not idempotent: every call creates something new. That's exactly what makes retrying it produce duplicates.

So wherever possible, pick an idempotent method. If the client comes up with the order id, create it through PUT /orders/{id}: a retry overwrites the same order instead of creating a second one.

PUT /orders/ord-42
{ "item": "book", "qty": 1 }

Per RFC 9110, the first such request, the one that created the resource, answers 201 Created, and a retry with the same id replaces it and answers 200 OK or 204 No Content. There is still one order. The id must be unique per operation: generate it once before the first attempt and don't change it on retries.

Idempotency key

When you need POST specifically (the server assigns the id), use an idempotency key. The client generates a unique key per operation and sends it in a header:

POST /orders
Idempotency-Key: 9f1c-44a2-...

{ "item": "book", "qty": 1 }

The server remembers the keys it has processed. When a request arrives with a key it has already seen, the server doesn't create a new order. It returns the result of the first one. A retry is safe: same key, same order. This is how payment APIs work, where a duplicate means money charged twice.

1st request: new key   → create the order, save (key → response)
2nd request: seen key  → return the saved response, create nothing

The Idempotency-Key header isn't a standard yet, just an IETF draft (draft-ietf-httpapi-idempotency-key-header). In practice Stripe and other payment APIs use it, so the header name comes up often.

Keys are stored with an expiration: there's no reason to keep them longer, retries arrive within a short retry window. Stripe lets you delete a key once it turns 24 hours old.

Where to store keys

Keys need to be visible to every copy of the service. If a key lives in a process's memory, a retry that the load balancer sends to a different copy won't find that key and will create a duplicate. This is the same "stateless processes" rule from the lesson on config and 12-factor: shared state lives in external storage.

A common choice of storage for keys is Redis. The SET command with the NX and EX flags writes a key in one step only if it doesn't exist yet, and sets its expiration:

SET idem:9f1c-44a2 "payment 7" NX EX 86400

An OK response means the key is new. A (nil) response means the key already existed. The point is that the check and the write happen in one command: two simultaneous requests with the same key can't both decide the key is new.

Why this goes together with retries

Idempotency and retries work as a pair. A retry, from the previous lesson, repeats a failed request, and idempotency guarantees the retry won't cause harm. Without idempotency, retrying a POST is dangerous. With an idempotency key, you can retry it as calmly as a GET.

The key is generated once per operation, before the first attempt. A new key on every attempt turns the retry into a new operation, and the protection disappears.

The same applies to queue handlers. A message can arrive more than once: with at-least-once delivery, the broker resends a message if it didn't get an acknowledgment (ack), and the ack gets lost when the handler crashes right after doing the work. The handler must be idempotent, or one event gets applied twice. Usually a message has an id, and the handler remembers processed ids the same way the server remembers keys.

Common mistakes

What to remember

Retrying a request should not create a duplicate. GET, PUT, DELETE are idempotent on their own. Where possible, create resources through PUT with an id the client came up with.

For POST, use an idempotency key: the server remembers the key in shared storage and returns the result of the first call on a retry. The key is generated once per operation and lives for a limited time.

Idempotency makes retries safe and is required for queue handlers with at-least-once delivery.

This closes out the application patterns module: graceful shutdown, health endpoints, config, resilience, and idempotency. The mini-project in lab 03 brings them all together on one service.

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson app-patterns-05-idempotency-api