ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

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-PatternConsequenceFix
No idempotency on paymentsDuplicate charges, refund nightmaresMandatory idempotency key for all financial operations
Server-generated idempotency keysClient retries create new keys, defeating purposeClient generates keys, includes in request header
No TTL on idempotency recordsUnbounded storage growth24-48 hour TTL (enough for retry windows)
Idempotency without lockingRace condition: two requests processed simultaneouslyDistributed lock on key before processing
Only check key, ignore request bodySame key reused for different operationsHash 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.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →