You hit "Pay" and the browser spins. Network timeout. Did the charge go through? You retry — and get charged twice.
Idempotency keys solve this. They're one of the most important API patterns you can implement, and they're still missing from most homegrown APIs.
What Is an Idempotency Key?
An idempotency key is a unique client-generated token attached to a request. If the server receives the same key twice, it returns the same result as the first request without re-executing the operation.
Stripe pioneered this pattern for payments. The concept is now widespread across financial, messaging, and any API where duplicate side effects are dangerous.
How It Works
The flow is simple:
- Client generates a unique key (UUID v4 is standard)
- Client sends the key in a request header:
Idempotency-Key: <uuid> - Server checks its store — has this key been seen before?
- No → Execute the operation, store the result against the key, return result
- Yes → Return the stored result immediately, no re-execution
POST /payments
Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479
Content-Type: application/json
{"amount": 2000, "currency": "usd", "customer_id": "cus_abc123"}
Retry that request 10 times with the same key — the charge happens exactly once.
Server Implementation (Node.js / Express)
const express = require("express");
const { createClient } = require("redis");
const { v4: uuidv4 } = require("uuid");
const app = express();
const redis = createClient();
await redis.connect();
app.post("/payments", async (req, res) => {
const idempotencyKey = req.headers["idempotency-key"];
if (!idempotencyKey) {
return res.status(400).json({ error: "Idempotency-Key header is required" });
}
// Check if we've seen this key before
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
return res.status(200).json(JSON.parse(cached));
}
// Process the payment
const result = await processPayment(req.body);
// Store result with a 24-hour TTL
await redis.setEx(
`idempotency:${idempotencyKey}`,
86400,
JSON.stringify(result)
);
return res.status(201).json(result);
});
Key implementation details:
- Store in Redis (or a DB), not in-memory — you'll have multiple server instances
- Set a TTL — 24 hours to 7 days is typical; clear old keys eventually
- Return the exact same HTTP status code on replays, not just 200
-
Lock during processing to handle concurrent requests with the same key (use Redis
SET NXor a DB transaction)
Client Implementation
import httpx
import uuid
def create_payment(amount: int, currency: str, customer_id: str) -> dict:
idempotency_key = str(uuid.uuid4())
for attempt in range(3):
try:
response = httpx.post(
"https://api.example.com/payments",
headers={"Idempotency-Key": idempotency_key},
json={"amount": amount, "currency": currency, "customer_id": customer_id},
timeout=10.0
)
response.raise_for_status()
return response.json()
except httpx.TimeoutException:
if attempt == 2:
raise
# Same key on retry — safe to resend
continue
raise RuntimeError("Payment failed after 3 attempts")
Critical: generate the key once, before the first attempt, then reuse it across retries. If you generate a new key per retry, you've defeated the entire purpose.
Edge Cases Worth Handling
Concurrent requests with the same key — Two requests arrive simultaneously before either has stored a result. Use a Redis lock or database row-level lock during the "check and process" window.
Different payloads, same key — This is a client bug. Your server should detect it and return 422 Unprocessable Entity with a clear error message:
{"error": "Idempotency key already used with different request body"}
Key collisions — Vanishingly rare with UUID v4 (~5.3 × 10^36 possible values), but document that keys must be globally unique per endpoint, not just per session.
What Not to Use Idempotency Keys For
Idempotency keys are for non-idempotent operations — POST, DELETE (when results matter), and anything with side effects. You don't need them for GET or PUT (which are naturally idempotent when implemented correctly).
Also, idempotency keys don't replace request signing or authentication — they're a separate concern.
Wrapping Up
One UUID header, a Redis key, and a TTL: that's all it takes to make your payment flow, order creation, or notification dispatch safe to retry. Your users' fingers hover over "pay again" — idempotency keys make that safe.
If you're building or testing APIs and want to inspect exactly what's being sent, stored, and returned across retries, APIKumo lets you script request flows with variables and pre/post-processors — handy for verifying your idempotency implementation behaves correctly under retry conditions.
Top comments (0)