abhi-g.dev
Distributed systemsApr 20195 min read

Idempotency: designing operations that survive retries

A network call can fail after the server has done the work. The client does not know, so it retries. Idempotency is what makes that retry safe, and it has to be designed in, not added later.

Originally written on 15 April 2019. Migrated from a WordPress blog and reformatted.

An operation is idempotent if performing it more than once has the same effect as performing it once. In a single process this is a curiosity. Across a network it is a requirement, because every remote call has a failure mode in which the request was processed but the response was lost, and the only correct client behaviour in that case is to try again.

The response is lost after the work is done; the retry must not do the work againclientserverdatabasekey -> resultPOST /charge key=K1insert + chargeresponse lost (timeout)client (retry)serverPOST /charge key=K1K1 exists: return the stored result,do not charge againkey write and charge in one transaction; key retention longer than any retry
The response is lost after the work is done; the retry must not do the work again

Where retries come from

In each case the operation runs twice. If it is "set status to SHIPPED", nothing is wrong. If it is "charge the card", the customer is charged twice.

Naturally idempotent operations

Some operations are idempotent by construction and need no further work.

OperationIdempotentReason
UPDATE orders SET status='SHIPPED' WHERE id=42yesabsolute value
UPDATE accounts SET balance = balance - 10norelative change
PUT /users/42 with a full bodyyesreplaces state
POST /ordersnocreates a new resource each time
DELETE /orders/42yessecond call is a no-op (or 404)
INSERT INTO log VALUES (...)noappends a row each time

The pattern: absolute writes are idempotent, relative writes and creations are not. Where the domain allows, prefer absolute writes. "Set balance to 90" instead of "subtract 10" requires the caller to know the current balance, which is often a feature.

Making non-idempotent operations safe

For creations and relative writes, attach a unique key to the request and have the server remember it.

Idempotency key. The client generates a UUID per logical operation and sends it with every attempt. The server stores the key with the result of the first execution and returns the stored result for any repeat.

-- inside the same transaction as the work
INSERT INTO idempotency_keys (key, response, created_at)
VALUES ($1, $2, now())
ON CONFLICT (key) DO NOTHING;

If the insert affects zero rows, the key was seen before: return the stored response without doing the work. Payment providers (Stripe's Idempotency-Key header is the well-known example) implement exactly this.

Two details that matter. The key store write must be in the same transaction as the side effect, or a crash between them re-opens the window. And keys need a retention period longer than any plausible retry, typically 24 hours or more.

Unique constraints as the key. When the operation has a natural identifier, use it. An invoice generated from event evt_123 can carry event_id as a unique column:

INSERT INTO invoices (event_id, amount) VALUES ($1, $2)
ON CONFLICT (event_id) DO NOTHING;

No separate key table, and the database enforces it.

Concurrent duplicates

Two copies of the same request can arrive at the same time, not one after the other. The key store handles this if the insert happens first: the second transaction blocks on the unique constraint until the first commits, then sees the conflict. Checking for the key with a SELECT before doing the work does not handle it; both requests see no key and both proceed.

Ordering

Idempotency protects against the same operation twice. It does not protect against two different operations arriving out of order, such as shipped after delivered on a retry. That requires a version or timestamp on the entity and a conditional write:

UPDATE orders SET status = $2, version = $3
WHERE id = $1 AND version < $3;

Summary

Assume every operation will be attempted more than once. Prefer absolute writes. For creations and relative changes, require a unique key, store it in the same transaction as the work, and rely on the database's unique constraint rather than a check-then-act sequence.