concept

Idempotency

created 2026-05-25 distributed-systems · idempotency · messaging · reliability · patterns

Idempotency

A handler is idempotent if processing the same message N times has the same effect as processing it once. In distributed systems where delivery-guarantees|at-least-once delivery is the practical default, idempotency is what turns “we might see this message twice” into “no user-visible bug.”

Exactly-once delivery over a network? No. Exactly-once effect through idempotent processing? Yes.

Why it’s the universal solvent

You can’t escape duplicates over an unreliable network (see two-generals-problem). Retry storms from network blips. Replays from a DLQ. ACK losses. Every message handler will eventually see a duplicate. If your handler:

  • Charges a card → idempotency prevents double charge.
  • Sends an email → idempotency prevents the 50-emails-per-signup horror story.
  • Increments a counter → idempotency prevents inventory drift.
  • Writes to a downstream API → idempotency prevents partial-write inconsistency.

Implementation patterns

1. Event-ID dedup table

Every message carries a unique event ID. The handler:

BEGIN
  if exists(processed_events, event_id): RETURN
  insert into processed_events (event_id, processed_at)
  do the work
COMMIT

Cheap, ergonomic, works for most cases. Periodically prune by TTL.

2. Natural-key idempotency

Use a domain identifier that’s stable across retries — order_id, payment_intent_id, request_id provided by the caller. If the operation is “create an order with this id,” a second call either no-ops or returns the same result.

Stripe’s Idempotency-Key header is this pattern at the API level — see the stripe notes.

3. Idempotent state transitions

Design the state machine so a no-op is naturally correct: mark_paid(order_id) is idempotent because re-marking a paid order is a no-op. add_charge(order_id, $10) is not — re-adding charges $20.

4. Transactional outbox

For “do the work + emit a message” semantics, write the message to a local outbox table in the same transaction as the work, then a separate worker forwards outbox rows to the broker with at-least-once + dedup keys. Combined with consumer-side idempotency, gives you the practical “exactly-once effect.”

What’s not idempotency

  • Locks — locks prevent concurrent processing, not duplicate processing across time.
  • “It hasn’t happened to us yet” — it will. The first retry storm is when teams find out.
  • Exactly-once delivery — doesn’t exist; see two-generals-problem.

The check to run

Open your message handlers. For each one ask: what happens if this runs twice with the same input? If the answer is anything other than “same result as once,” it’s not idempotent. Fix it before the next network blip turns it into a P0.

See also