Cloud Tech by Victor
System DesignIntermediate

Idempotency in Distributed Systems

Why Stripe returns the exact same response, error included, for a reused idempotency key instead of retrying the operation, and why reusing that same key with different parameters is treated as an error, not a new request.

Updated 2026-07-243 min read

Overview

An idempotency key turns an otherwise unsafe-to-retry operation, a payment creation, an order submission, into one a client can safely retry after an uncertain outcome (a timeout, a dropped connection) without risking a duplicate side effect: the server records the key alongside the operation's result, and if that same key shows up again, it returns the exact original result, error included, rather than re-executing anything. Reusing a key with different parameters is rejected as an error rather than silently processed, because an idempotency key is a promise that one specific operation happened once, and mismatched parameters mean either that promise no longer holds or the client made a mistake generating the key. Keys are most commonly needed for genuinely non-idempotent operations like POST; GET is already safely repeatable by definition and never needs one, while whether DELETE needs one is API- and version-specific, since a DELETE can still carry non-idempotent side effects some APIs choose to protect with a key.

Quick Reference

ScenarioResult
Same key, same parameters, retriedExact original result returned (even if it was an error), not re-executed
Same key, different parametersRejected as an error
Key not seen beforeProcessed normally, result recorded against the key
Key older than the retention windowTreated as new; the old result is no longer available

Syntax

bash
curl https://api.example.com/v1/charges \
  -H "Idempotency-Key: 4f3a9c2e-8b1d-4e6a-9f2c-1a2b3c4d5e6f" \
  -d amount=2000 -d currency=usd

Examples

python
# Client-side: generate the key once per logical operation,
# reuse it across retries of that same attempt only.
import uuid

idempotency_key = str(uuid.uuid4())
for attempt in range(3):
    try:
        response = create_charge(amount=2000, idempotency_key=idempotency_key)
        break
    except TimeoutError:
        continue  # safe to retry with the same key

Generate a new key per logical operation, not per request

Reusing a key across genuinely different operations (different parameters) is treated as an error, and generating a fresh key for what should be a retry of the same operation defeats the entire purpose, it stops protecting against duplicates.

Visual Diagram

Common Mistakes

  • Generating a new idempotency key on every retry attempt, which defeats the purpose entirely, each "retry" becomes a genuinely new operation the server has never seen.
  • Reusing the same key across requests with different parameters, expecting the new parameters to be processed rather than rejected.
  • Adding idempotency keys to GET requests, which are already safely repeatable and don't need this mechanism; assuming the same is automatically true of every DELETE without checking that endpoint's documented contract.
  • Assuming idempotency keys are retained forever; most systems prune them after a bounded window, after which reuse is treated as a brand-new request.

Performance

  • Looking up whether an idempotency key has already been seen is a fast, indexed operation, negligible overhead compared to the operation it protects against duplicating.
  • Idempotency key storage has a real, bounded retention window; this is a deliberate trade-off between protecting against reasonably-timed retries and not storing request history indefinitely.

Best Practices

  • Generate exactly one idempotency key per logical operation, and reuse that same key for every retry attempt of that same operation.
  • Use a sufficiently random identifier (a UUID) as the key, and avoid embedding sensitive data in it.
  • Apply idempotency keys based on the endpoint's documented retry contract rather than the HTTP verb alone; POST-style state changes almost always need one, GET never does, and DELETE depends on the specific API and its side effects.
  • Design the client to treat a mismatched-parameters error as a real bug to fix, not something to retry past.

Interview questions

A client sends a payment request, the network times out before a response arrives, and the client retries with the same idempotency key. What actually happens on the server?

If the original request already completed (successfully or even with an error like a 500) before the retry arrives, the server returns the exact same result it returned (or would have returned) the first time, the same status code and response body, without re-executing the underlying operation, so the customer isn't charged twice just because the client never saw the first response. This is the entire point of an idempotency key: it lets a client safely retry an operation whose actual outcome it's genuinely uncertain about (did the first request even reach the server? did it complete before the timeout?) without that retry risking a duplicate side effect.

Why does reusing the same idempotency key with different request parameters return an error instead of just processing the new parameters?

An idempotency key is a promise that a specific, exact operation happened once; if the same key showed up with different parameters, honoring the new parameters would silently violate that promise; either the original operation's recorded result no longer describes what the key represents, or the client made a mistake by rIeusing a key it should have generated fresh for a genuinely different request. Rejecting the mismatched reuse as an error, rather than guessing which parameters were "correct" or silently processing the new ones, surfaces that client-side mistake immediately instead of masking it.

Why are idempotency keys typically associated with state-changing requests like POST, and are they ever relevant to DELETE?

GET is defined to have no side effects at all, retrying it any number of times simply returns the current state and changes nothing, so there is no duplicate-side-effect risk an idempotency key could protect against, GET genuinely doesn't need one. DELETE is idempotent in the narrow sense that deleting an already-deleted resource is a no-op or a consistent "not found," but whether an idempotency key is relevant to it is API- and version-specific, not settled by the HTTP verb alone: a DELETE can still trigger complex, non-idempotent side effects (a refund, a cascading cleanup, a billing adjustment), and some APIs explicitly accept an idempotency key on DELETE for exactly that reason. Idempotency keys exist to make an operation's retry semantics explicit and safe regardless of whether the underlying operation is inherently idempotent, checking the specific endpoint's documented contract matters more than assuming based on the verb.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement