Backend Idempotency Patterns
Design APIs that can be safely retried without side effects. Covers idempotency keys, at-most-once delivery, deduplication strategies, and the patterns that prevent duplicate charges, duplicate emails, and double-counted metrics in distributed systems.
In distributed systems, network failures are normal. A client sends a request, gets a timeout, and retries — but the server already processed the first request. Without idempotency, the customer gets charged twice, receives two emails, and their order is created twice. Idempotency guarantees that multiple identical requests produce the same result as a single request.
Idempotency Key Pattern
Client → Server: POST /payments (Idempotency-Key: "abc-123")
Scenario 1: First request succeeds
Server: Process payment → Store result with key "abc-123" → Return 200
Scenario 2: Network timeout, client retries with same key
Server: Check key "abc-123" → Found cached result → Return cached 200
Payment is NOT processed again
Scenario 3: Different request with same key
Server: Check key "abc-123" → Found, but request body differs → Return 409 Conflict
Implementation flow:
1. Client generates unique idempotency key (UUID)
2. Client sends key in request header
3. Server checks if key exists in idempotency store
a. Key exists → return cached response
b. Key doesn't exist → process request, store response with key
4. Key expires after TTL (24 hours typical)
Implementation
import hashlib
import json
from datetime import datetime, timedelta
class IdempotencyMiddleware:
"""Ensure API requests are processed at most once."""
def __init__(self, store, ttl_hours: int = 24):
self.store = store # Redis or database
self.ttl = timedelta(hours=ttl_hours)
def process_request(self, request):
"""Check idempotency before processing."""
idempotency_key = request.headers.get("Idempotency-Key")
if not idempotency_key:
# No key provided — process normally (not idempotent)
return None
# Namespace the key by endpoint + method
full_key = f"idempotency:{request.method}:{request.path}:{idempotency_key}"
# Check if we already processed this request
cached = self.store.get(full_key)
if cached:
cached_data = json.loads(cached)
# Verify request body matches (detect misuse)
request_hash = self.hash_body(request.body)
if cached_data["request_hash"] != request_hash:
return Response(status=409, body={
"error": "Idempotency key reused with different request body"
})
# Return cached response
return Response(
status=cached_data["status_code"],
body=cached_data["response_body"],
headers={"X-Idempotent-Replayed": "true"},
)
# Lock the key to prevent race conditions
if not self.store.set_nx(full_key, json.dumps({
"status": "processing",
"request_hash": self.hash_body(request.body),
"started_at": datetime.utcnow().isoformat(),
}), ex=60):
# Another request with this key is being processed
return Response(status=409, body={
"error": "Request with this idempotency key is being processed"
})
return None # Proceed with normal processing
def cache_response(self, request, response):
"""Store response for future replays."""
idempotency_key = request.headers.get("Idempotency-Key")
if not idempotency_key:
return
full_key = f"idempotency:{request.method}:{request.path}:{idempotency_key}"
self.store.set(full_key, json.dumps({
"request_hash": self.hash_body(request.body),
"status_code": response.status_code,
"response_body": response.body,
"processed_at": datetime.utcnow().isoformat(),
}), ex=int(self.ttl.total_seconds()))
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| No idempotency on payments | Duplicate charges, refund nightmares | Mandatory idempotency key for all financial operations |
| Server-generated idempotency keys | Client retries create new keys, defeating purpose | Client generates keys, includes in request header |
| No TTL on idempotency records | Unbounded storage growth | 24-48 hour TTL (enough for retry windows) |
| Idempotency without locking | Race condition: two requests processed simultaneously | Distributed lock on key before processing |
| Only check key, ignore request body | Same key reused for different operations | Hash and verify request body matches |
Idempotency is the foundation of reliable distributed systems. Every API that creates state, charges money, or triggers side effects must be idempotent. The cost of implementing idempotency is trivial compared to the cost of debugging duplicate payments at 3 AM.