April 2026 • Rich Robertson

Retries, Idempotency, and Duplicate Writes

Retries are mandatory in distributed systems. Without strict idempotency and bounded retry policy, they convert transient faults into duplicate side effects and cascading load.

Framing the problem

Timeouts are ambiguous: a request may have failed before execution, failed during execution, or succeeded but lost its response. Retrying is rational, but every retry is extra work against an already unhealthy dependency. If writes are not idempotent, each replay can produce a new side effect.

This page focuses on practical controls for real production systems where network faults, failover events, and overloaded downstreams are normal operating conditions.

Why duplicate writes happen

The outcome is familiar: duplicate charges, repeated provisioning, double email/send actions, or state machines that jump two steps instead of one.

Idempotency design that holds up in production

Define operation identity, not just request identity

Idempotency keys should represent business intent (for example, "create order X for tenant Y") instead of transport-level request IDs. Otherwise retries may bypass dedupe because metadata differs while intent is the same.

Persist first outcome durably

Store key + normalized request fingerprint + outcome status in a durable dedupe store. Duplicate keys should return the original outcome body and status code, not recompute side effects.

Align dedupe TTL with real retry windows

If keys expire too quickly, delayed retries from mobile clients, queues, or async workflows can still double-apply writes. TTL should cover realistic replay horizons, not only synchronous API retries.

Commit write + dedupe atomically

If the side effect commits but dedupe record does not, the next retry may execute again. Use one transaction boundary where possible, or a compensating recovery log when storage boundaries differ.

Retry policy: recover without causing storms

Policy elementRecommended defaultWhy it matters
Error classificationRetry transient transport + selected 5xx/429 onlyAvoid retrying deterministic failures
BackoffExponential with full/decorrelated jitterPrevents synchronized retry bursts
Retry budgetCap retries as fraction of baseline trafficLimits load amplification during incidents
DeadlinesBound total request time, not only per-attempt timeoutAvoids zombie work after caller gave up
CancellationPropagate upstream cancellation downstreamReduces useless queued retries

Example: create-and-notify workflow

Consider an API that creates a resource and emits a notification:

  1. Client sends POST /resources with idempotency key tenant123:create:abc.
  2. Server writes resource and idempotency record in one transaction.
  3. Notification is emitted from an outbox consumer keyed to the same operation identity.
  4. If client times out and retries, server returns prior response; outbox consumer dedupes notification by operation key.

This keeps API correctness and asynchronous side effects aligned. Without shared operation identity, one layer may dedupe while another still duplicates.

Common mistakes

When engineers should care most

Conclusion

Retries are a correctness feature only when paired with strong idempotency semantics and overload-aware budgets. Build for replay as a first-class reality: define operation identity, persist outcomes durably, and bound retry behavior so failure recovery does not create new incidents.