Skip to main content

Idempotency & Safe Retries

Level 1: Level 1: Foundations & Mental Modelsmedium30 minidempotencyretriespayments

Give mutations a client-generated idempotency key, store the full response behind a unique-constraint insert, and turn at-least-once delivery into effectively-once.

The two-generals problem in every network call

The problem idempotency solves is the two generals nature of a network call. A client sends "submit payment," the server processes it, and then the response is lost to a timeout or a dropped connection. The client does not know whether the charge happened. If it retries naively, you double-charge. If it gives up, you might drop a real payment. Idempotency lets the client retry safely and get the same result every time.

Start with what HTTP already gives you. GET, PUT, and DELETE are idempotent by definition: sending them twice leaves the server in the same state as sending them once (PUT sets a value; setting it twice is the same value; DELETE twice still ends deleted). POST and PATCH are not idempotent, because "create" or "add $50" applied twice does two things. Those are exactly the methods that need explicit help.

The idempotency key

The client generates a unique key (a UUID) for the logical operation and sends it, typically as an Idempotency-Key header. The server, on first receipt, processes the request and stores the full response keyed by that key with a TTL (24 hours is a common window). On any retry with the same key, the server does not reprocess; it returns the stored response.

Check yourself
A server dedupes payments by storing a boolean 'seen this key' flag. The original request and its retry arrive at the same instant. What happens?

The subtle, commonly-missed detail: store the response, not just a boolean "seen it" flag. Two things force this. First, the retry must get the actual result (the charge id, the status), not just "yes." Second, concurrency: the original request and the retry can arrive at the same instant. You need a way for the second one to either wait for the first to finish or detect an in-flight operation, so they converge on one answer instead of both charging. In practice you insert the key into a store with a unique constraint (a Redis SETNX or a unique DB row) before doing work; the loser of that race waits and returns the winner's stored response.

From at-least-once to effectively-once

Networks and queues give you at-least-once delivery: a message can arrive more than once. Idempotency (deduplication on a key) is what turns at-least-once into effectively-once processing. You cannot get true exactly-once over an unreliable network; you get at-least-once plus idempotent handling, which is behaviorally equivalent and is what payment systems actually do.

The same pattern extends beyond synchronous APIs. Webhooks should carry an event id so the receiver can dedupe redelivered events, and message-queue consumers should dedupe on a key so a redelivered Kafka or SQS message is processed once.

Interview nuance: interviewers push on the concurrency case. "Store a flag" is the answer that fails; "store the response behind a unique-constraint insert so concurrent duplicates converge" is the one that passes.

Recap: give mutating requests a client-generated idempotency key, store the full response behind a unique constraint with a TTL, and return it on any retry so at-least-once delivery becomes effectively-once and nobody double-charges.

Check yourself
A payments team says their API guarantees exactly-once charging over the public internet. If it really works, what are they actually running?

Apply

Your turn

The task this lesson builds to.

Make a 'submit payment' POST safe to retry after a client timeout, and specify the server behavior on the duplicate.

Think about

  1. Which HTTP methods are idempotent by definition, and which need explicit handling?
  2. What does the server store so concurrent duplicates get the same answer?
  3. How does at-least-once become effectively-once?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design idempotency for an event-driven order pipeline where a checkout publishes an order.placed event to Kafka, and three consumers (charge the card, decrement inventory, send confirmation) each process it. Kafka guarantees at-least-once delivery, so every consumer will occasionally see the same event twice. Make the whole pipeline effectively-once without a distributed transaction.

Think about

  1. Why must idempotency live in each consumer rather than in one global transaction?
  2. How does the transactional-inbox pattern make 'already handled?' and 'the effect' atomic?
  3. When is it safe to commit the Kafka offset?