A client sends a request, the server completes it, and the response is lost. The client retries. Did the operation happen once or twice?
That question appears in payments, rewards, notifications, imports, and queue workers. Retries are expected in distributed systems, so correctness cannot depend on a function running exactly once.
Define the business effect#
Idempotency means repeating an operation with the same identity produces the same final effect. It does not necessarily mean returning byte-for-byte identical responses.
For a task completion, the natural identity may be the task ID. For creating an order, the client can send an idempotency key:
const result = await operations.findOneAndUpdate(
{ userId, key: idempotencyKey },
{
$setOnInsert: {
userId,
key: idempotencyKey,
status: "processing",
createdAt: new Date(),
},
},
{ upsert: true, returnDocument: "after" },
);
A unique index on { userId, key } makes the database enforce the guarantee even when two invocations race. An application-level “check, then insert” without a unique constraint still allows duplicates.
Choose the duplicate behavior#
A repeated request can:
- return the previously stored result;
- report that processing is still in progress;
- become a documented no-op;
- resume incomplete work safely.
Store enough state to make that decision. If the key exists but belongs to a different request payload, reject it rather than silently treating two operations as the same. A request hash can detect that mismatch.
Idempotency must reach side effects#
Making the database write idempotent is not enough if every retry still sends an email. Give downstream effects their own stable identity, such as order-confirmed:{orderId}, and enforce uniqueness where the effect is queued or recorded.
For multi-step operations, an outbox is often safer: commit the business change and an event record together, then let a worker deliver that event with retry protection.
Test the second attempt#
The most useful idempotency test executes the same command twice and asserts one business effect. Add a concurrency test as well; sequential success does not prove that two simultaneous invocations are safe.
Retries improve reliability only when repeated work is harmless. Decide the operation identity, enforce it atomically, define the response for duplicates, and carry that identity into downstream effects.