Articulation
Search

Idempotency, and Why Retries Need It

A network can't tell a client whether its request failed or only its reply did. Idempotent operations make that ambiguity safe to retry through.

Published Updated 3 min read

A client sends a request and waits. Nothing comes back. Did the server never receive the request, receive it and crash, or do the work and lose the reply on the way home? From the client’s side these cases look identical: a timeout. This demonstration essay walks through why that ambiguity is unavoidable and how idempotency turns it from a hazard into a routine.

The ambiguity at the heart of a timeout

When a request times out, the client has three explanations it cannot tell apart. The note Retries are inevitable collects them in more detail, but the shape is simple:

What happenedDid the effect occur?Is a retry safe?
Request lost before reaching serverNoYes
Server failed before actingNoYes
Server acted; response lostYesOnly if idempotent

The last row is the dangerous one. If the operation was “charge this card”, a blind retry charges twice.

What idempotent means for a request

HTTP gives a precise definition. A method is idempotent when the intended effect on the server of several identical requests is the same as the effect of one Fielding et al. 2022, §9.2.2. The specification also gives the practical consequence: idempotent requests can be repeated automatically when a communication failure occurs before the client reads the response.

The standard methods fall out like this:

MethodSafeIdempotent
GET✓✓
HEAD✓✓
OPTIONS✓✓
PUT✓
DELETE✓
POST

“Safe” methods are read-only in intent, so they are trivially idempotent. PUT replaces a resource with the enclosed representation; doing that twice leaves the same state as doing it once.1 POST carries no such promise.

How likely is a duplicate?

Suppose each attempt independently ends in a timeout with probability pp, and the client retries up to nn times. The probability that every attempt times out is

P(all n attempts time out)=p n,P(\text{all } n \text{ attempts time out}) = p^{\,n},

so retrying drives the chance of giving up towards zero quickly: with p=0.05p = 0.05, three attempts leave a 0.053=0.0001250.05^3 = 0.000125 chance of failure. But each timed-out attempt may still have executed on the server. In the worst case, a successful sequence of nn attempts has run the operation nn times. Reliability through retries and exactly-once effects pull in opposite directions unless the operation is idempotent.

Making a non-idempotent operation safe

A common pattern is the idempotency key: the client generates a unique key per logical operation and sends it with every attempt. The server records the outcome under that key and replays it for repeats instead of acting again.

Sequence: client retries with the same idempotency key; server replays the stored result.ClientServerPOST /charges Idempotency-Key: k-81f…response lost ✕retry, same key k-81f…stored result replayed, no second chargechargelookup k-81f…
The second attempt carries the same key, so the server returns the stored result instead of charging again.

A minimal server-side sketch, with the storage left abstract:

interface Outcome {
  status: number;
  body: unknown;
}

interface OutcomeStore {
  /** Atomically claim a key; returns the existing outcome if already claimed. */
  claim(key: string): Promise<{ claimed: true } | { claimed: false; outcome?: Outcome }>;
  complete(key: string, outcome: Outcome): Promise<void>;
}

export async function handleOnce(
  key: string,
  store: OutcomeStore,
  perform: () => Promise<Outcome>,
): Promise<Outcome> {
  const claim = await store.claim(key);
  if (!claim.claimed) {
    // A previous attempt got here first: replay, or ask the client to wait.
    return claim.outcome ?? { status: 409, body: { error: 'request in progress' } };
  }
  const outcome = await perform();
  await store.complete(key, outcome);
  return outcome;
}

The essential property is that claim is atomic. If two attempts race, exactly one performs the work; the other observes the claim and waits or replays.

Where to go from here

Retrying well has a second half: when to retry. Retrying immediately, in lockstep with every other client, can turn a brief outage into a sustained one. The backoff playground lets you see how exponential backoff and jitter spread retries out over time.

About this entry

This is demonstration content showing code, tables, maths, a footnote, a sidenote, a figure and a citation in one article. The citation points to a real public standard; the remaining text describes widely documented engineering practice, not the site author’s views.

Notes

  1. The response to a repeated request may differ — a second DELETE might return 404 — while the effect on server state is the same. Idempotency is a property of effects, not of responses. ↩

References

  1. Fielding et al. 2022 R. Fielding, M. Nottingham, J. Reschke. HTTP Semantics . IETF, RFC 9110 , 2022.
  • Note Retries are inevitable

    Over a network, a client that times out cannot know what happened. So it will retry — and the system must be built to survive that.

    Related

  • Experiment Backoff and Jitter Playground

    Adjust an exponential backoff schedule and watch what happens when two dozen clients retry at once — with and without randomness.

    Related

  • Note Identity over time

    Questions about what makes something persist through change — and an unexpected echo of them in software.

    Links to this page