3 min readRishi

Idempotency Keys for Write APIs

Idempotency Keys for Write APIs

A client times out. It retries the POST. The first request actually committed. The second creates another charge, another order, another email. The fix is an idempotency key: the client sends a unique key with the write, and the server promises that a second request with the same key returns the original result and does not do the work again.

The key is chosen by the client, not the server. A server-generated id arrives too late — the client that timed out does not have it. A UUID per user action, stored by the client until it gets a definite response, is enough. Reuse that same key for every retry of that action. A new key means a new action.

What the server stores

On the first request, persist the key and the outcome in the same transaction as the business write, or in an order that cannot leave a committed write without the key.

Stored with the keyWhy
Request hashA reused key with a different body is a client bug. Reject it. Do not silently run a different operation
Response status and bodyThe retry must see the same answer, including a 400 that was the real result
State: in-progress or finalTwo retries in flight need a single winner

The race is two requests with the same key passing the "have I seen this" check together. A unique constraint on the key is the lock. The loser waits, or gets a 409 while the winner is in progress, and retries after a short delay. When the winner commits, the loser reads the stored response and returns it.

Do not run the business logic twice and then notice the key. The unique insert comes first.

Expiry

Keys cannot live forever or the table becomes every write you have ever accepted. They also cannot expire while a client might still retry. A client timeout plus a few retries is minutes. A mobile client that backgrounds and resumes might be hours. Twenty-four hours is a common window for payment-style APIs. Document it. After expiry, the same key is a new request, and a late retry can double-apply. Clients must stop retrying before that, or send a new key only when the user explicitly tries again.

A 500 that you did not persist is not idempotent. If you return 500 before storing the key, the client will retry and you will do the work twice. Persist the key in the in-progress state before the side effect that can fail halfway — the charge, the email, the message publish. If that side effect fails, store the failure on the key or delete the in-progress row inside the same recovery path. "Maybe" is how duplicates happen.

What not to use as the key

The user id is not an idempotency key. The user will place two orders. A hash of the entire cart is fragile: two intentional identical carts become one, and a cart that changed by a cent becomes two. The key is "this click," generated when the user confirmed, held across retries, discarded when the UI starts a new confirmation.

GET and PUT of a full resource are already safe to repeat if PUT replaces state. POST and PATCH that append or increment are the ones that need the key. Add the header, store the response, and test with two parallel calls. One side effect, two identical responses, is the passing test.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

  • No comments yet. Be the first.