Skip to content
general software-development

Idempotency

idempotency api-design reliability distributed-systems
Plain English

An operation is idempotent if doing it twice has the same effect as doing it once. Setting a thermostat to 72 degrees is idempotent: whether you set it once or ten times, the temperature ends up at 72. Adding $100 to an account is NOT idempotent: doing it twice adds $200. Idempotency matters in software because networks are unreliable: if a request times out and you retry it, an idempotent operation is safe to repeat, but a non-idempotent one might duplicate the action.

Technical Definition

Idempotency is a property of an operation where applying it multiple times produces the same result as applying it once. Formally: f(f(x)) = f(x).

HTTP methods and idempotency:

MethodIdempotentSafeExample
GETYesYesRead a resource (no side effects)
PUTYesNoReplace a resource (same result if repeated)
DELETEYesNoDelete a resource (deleting twice = same as once)
HEADYesYesRead headers only
POSTNoNoCreate a resource (repeating creates duplicates)
PATCHNoNoPartial update (depends on implementation)

Making non-idempotent operations safe:

  1. Idempotency keys: client generates a unique key (UUID) per request and sends it in a header (Idempotency-Key: abc-123). Server stores the key and result; if the same key arrives again, return the cached result without re-processing.

  2. Conditional requests: use If-None-Match (ETag) or If-Unmodified-Since to ensure the operation only executes if the resource state matches expectations.

  3. Database constraints: unique constraints prevent duplicate insertions. INSERT ... ON CONFLICT DO NOTHING is idempotent.

Why it matters in distributed systems:

  • Network timeouts cause retries (did the server receive my request or not?)
  • Message queues may deliver messages more than once (at-least-once delivery)
  • Webhooks may fire multiple times for the same event
  • Without idempotency, retries cause duplicate charges, duplicate records, or inconsistent state

Implementing idempotency

// API endpoint with idempotency key
app.post("/api/payments", async (req, res) => {
  const idempotencyKey = req.headers["idempotency-key"];
  if (!idempotencyKey) {
    return res.status(400).json({ error: "Idempotency-Key header required" });
  }

  // Check if we already processed this request
  const existing = await db.query(
    "SELECT result FROM idempotency_store WHERE key = $1",
    [idempotencyKey]
  );

  if (existing.rows.length > 0) {
    // Return cached result (safe retry)
    return res.status(200).json(existing.rows[0].result);
  }

  // Process the payment
  const result = await processPayment(req.body);

  // Store the result for future retries
  await db.query(
    "INSERT INTO idempotency_store (key, result, created_at) VALUES ($1, $2, NOW())",
    [idempotencyKey, result]
  );

  return res.status(201).json(result);
});
# Client: safe retry with idempotency key
$ IDEM_KEY=$(uuidgen)
$ curl -X POST https://api.example.com/payments \
  -H "Idempotency-Key: $IDEM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 100, "currency": "USD"}'
# Safe to retry with the same key if timeout occurs
In the Wild

Idempotency is a core principle in building reliable systems. Payment processors (Stripe, Square) require idempotency keys on all POST requests to prevent double charges. Webhook handlers must be idempotent because providers retry on failure. Ansible, Terraform, and Kubernetes are all designed around idempotent operations: running them multiple times converges to the desired state without side effects. In database migrations, idempotent scripts use CREATE TABLE IF NOT EXISTS and INSERT ... ON CONFLICT to be safely re-runnable. The lack of idempotency is a frequent source of production bugs: a payment webhook fires twice, creating two charges; a CI/CD pipeline retries and deploys the same version twice with different configurations. Designing for idempotency from the start is much easier than retrofitting it.