Idempotency
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.
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:
| Method | Idempotent | Safe | Example |
|---|---|---|---|
| GET | Yes | Yes | Read a resource (no side effects) |
| PUT | Yes | No | Replace a resource (same result if repeated) |
| DELETE | Yes | No | Delete a resource (deleting twice = same as once) |
| HEAD | Yes | Yes | Read headers only |
| POST | No | No | Create a resource (repeating creates duplicates) |
| PATCH | No | No | Partial update (depends on implementation) |
Making non-idempotent operations safe:
-
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. -
Conditional requests: use
If-None-Match(ETag) orIf-Unmodified-Sinceto ensure the operation only executes if the resource state matches expectations. -
Database constraints: unique constraints prevent duplicate insertions.
INSERT ... ON CONFLICT DO NOTHINGis 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 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.