Engineering
Idempotency is a table, not a convention
A retry after a dropped connection is indistinguishable from a second request. The only way to tell them apart is to have written the first one down.
Engineering
A client submits a form. The response never arrives — the connection dropped somewhere after the server committed. The client retries, because that is the correct thing for a client to do. The customer now has two purchase orders.
No amount of care on the client prevents this. The request genuinely was sent twice, and from the server's position the two are identical.
One key per logical operation
The key has to be minted once, before the first attempt, and reused by every retry. Minting it per attempt is the mistake that looks like a fix: three retries become three distinct operations and the ledger is worthless.
// Minted before the first attempt, reused by all of them.
const idempotencyKey =
rest.idempotencyKey ?? (MUTATING.has(method) ? crypto.randomUUID() : undefined);
return withRetry(() => execute(path, { ...rest, idempotencyKey }), {
maxAttempts: MUTATING.has(method) ? 3 : 4,
});Store the response, not just the key
Recording that a key was seen is enough to prevent a duplicate, but it leaves the retrying client with nothing useful. Storing the first response means a retry replays it — the client gets the same answer it would have got, and never learns that anything went wrong.
Same key with a different body is the interesting case. That is not a retry, it is a client bug, and it should be a conflict rather than a silent overwrite.
Where it goes wrong
The ledger needs a TTL, or it grows forever. Ten minutes covers realistic retry windows.
It also has to be written in the same transaction as the side effect. A ledger entry committed separately can succeed while the operation rolls back, at which point the retry is refused and the work never happens at all — which is worse than the duplicate you were preventing.
