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 key | Why |
|---|---|
| Request hash | A reused key with a different body is a client bug. Reject it. Do not silently run a different operation |
| Response status and body | The retry must see the same answer, including a 400 that was the real result |
| State: in-progress or final | Two 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
Write Skew: The Anomaly Snapshot Isolation Does Not Catch
Two transactions each read what the other writes, both commit, and an invariant dies. Snapshot isolation allows write skew — here is how to close the hole.
Designing a Shopping Cart: Price Snapshots, Merge, and No Inventory Hold
A cart is a per-user document, not a reservation. What to store on the line, how to merge an anonymous cart at login, and when the price is allowed to change.
Designing Snowflake-Style IDs: Ordering, Workers, and Clock Rollback
A 64-bit id from a timestamp, a worker number, and a per-millisecond sequence. How many you can issue, why the clock must not step backwards, and why JavaScript cannot hold one.
PostgreSQL 19 on October 29: WAIT for Read-Your-Writes, REPACK Without the Lock, and What Got Pulled
PostgreSQL 19 GA is scheduled for October 29, 2026. The WAIT command gives standbys read-your-writes, REPACK CONCURRENTLY replaces VACUUM FULL and CLUSTER, autovacuum goes parallel, and SQL/PGQ, online checksums, and FOR PORTION OF were reverted in Beta 4.
Read-Your-Writes: The User Just Saved and the Read Replica Does Not Know
Why a write to the primary followed by a read from a replica shows stale data, and the practical ways to pin the next read without sending all traffic to the primary.
Designing Chat Presence: Online, Away, and the Lie of Instant Status
Heartbeat vs subscriptions, fan-out of presence, privacy, and why a boolean online flag does not survive a million concurrent sockets.
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.