Why Idempotency Matters
APIs are not perfect. Networks drop packets, servers timeout, clients retry. If your API isn't idempotent, a simple retry can create duplicate orders, double-charge credit cards, or corrupt state.
Idempotency means that applying the same operation multiple times has the same effect as applying it once. For example, a PUT request that sets a resource's state is idempotent by design. A POST request that creates a resource is not, because it creates a new resource each time.
HTTP Methods and Idempotency
HTTP defines idempotency for standard methods:
-
GET,HEAD,OPTIONS,TRACE: safe and idempotent -
PUT,DELETE: idempotent -
POST,PATCH: not idempotent by default
DELETE is idempotent because deleting a resource that doesn't exist returns 404, but the state is still "resource absent." PATCH can be idempotent if you use absolute values, but it's safer to treat it as non-idempotent.
The Idempotency-Key Pattern
For non-idempotent operations like POST, the standard solution is the Idempotency-Key header. The client generates a unique key for each logical operation and sends it with the request. The server stores the key and the response for that key. If a request with the same key arrives again, the server returns the stored response instead of executing the operation again.
Implementation Example (Node.js/Express)
Here's a minimal in-memory implementation:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const idempotencyStore = new Map();
app.post('/orders', (req, res) => {
const key = req.headers['idempotency-key'];
if (!key) {
return res.status(400).json({ error: 'Missing Idempotency-Key header' });
}
if (idempotencyStore.has(key)) {
const stored = idempotencyStore.get(key);
return res.status(stored.status).json(stored.body);
}
// Simulate processing
const order = { id: crypto.randomUUID(), ...req.body };
const response = { status: 201, body: order };
idempotencyStore.set(key, response);
return res.status(201).json(order);
});
app.listen(3000);
In production, use Redis or a database with TTL to store keys and responses. Also, handle concurrent requests with the same key: use a lock or a unique constraint to ensure only one request processes.
Idempotency Keys Best Practices
- Client side: Generate a UUID for each logical operation. Reuse the same key for retries of that operation.
- Server side: Store the key, request hash, and response. Return 409 Conflict if the same key is used with a different request payload.
- Expiration: Set a reasonable TTL (e.g., 24 hours) to avoid unbounded storage.
- Scope: Idempotency keys are per-user or per-client, so include user ID in the storage key.
Making PATCH Idempotent
If you want PATCH to be idempotent, use absolute values instead of relative increments. For example, {"quantity": 5} sets quantity to 5, while {"increment": 1} is not idempotent. Document this behavior clearly.
Handling Retries in Clients
Clients should retry on network errors or 5xx responses, but not on 4xx (except maybe 429). When retrying, send the same Idempotency-Key. Also, set a reasonable timeout and retry with exponential backoff.
Testing Idempotency
Write tests that:
- Send the same
Idempotency-Keytwice and assert the same response. - Send the same key with a different payload and expect 409.
- Simulate server crash after processing but before responding (store the key before processing).
Conclusion
Idempotency is not an optional feature; it's essential for building reliable APIs. By implementing the Idempotency-Key pattern, you protect your users from duplicate operations and make your API resilient to retries. It's a small effort that pays off in trust and correctness.
Top comments (0)