Idempotency in Distributed Systems: Building Reliable APIs
Learn how to design idempotent API endpoints using unique request IDs and database constraints to ensure system reliability even when network retries occur.

Previously in this course, we explored designing for failure and implementing circuit breakers. These strategies ensure your system remains resilient during outages, but they often trigger retries. This lesson adds the critical safety layer needed to handle those retries: idempotency.
The Problem: When "Once" Becomes "Twice"
In distributed systems, network partitions are inevitable. A client might send a POST /payments request, the server might process it successfully, but the network connection drops before the client receives the 200 OK. The client, assuming the request failed, retries.
Without idempotency, you process the payment twice. Idempotency is the property where an operation can be applied multiple times without changing the result beyond the initial application. Achieving this is non-negotiable for any state-changing operation in a robust architecture.
Designing Idempotent API Endpoints
To design idempotent endpoints, we move away from "blind" execution. Instead, we implement a "Check-Act" pattern driven by a unique Idempotency Key.
The flow works like this:
- The client generates a unique ID (UUID) for the specific request.
- The client sends this ID in a custom header, typically
Idempotency-Key. - The server checks if this key has already been processed.
- If yes, the server returns the cached result of the previous operation.
- If no, the server processes the request and stores the result associated with that key.
Worked Example: Protecting a Payment Endpoint
Let’s implement this using a simple Redis store to track keys and a relational database for the state.
PYTHONimport uuid from flask import request, jsonify import redis # Simple cache client cache = redis.Redis(host=CE9178">'localhost', port=6379, db=0) def process_payment(): key = request.headers.get(CE9178">'Idempotency-Key') if not key: return jsonify({"error": "Missing Idempotency-Key"}), 400 # 1. Check if we've seen this key cached_response = cache.get(f"idempotency:{key}") if cached_response: return cached_response, 200 # 2. Process the business logic # In a real app, use a database transaction here result = execute_payment_logic(request.json) # 3. Cache the result for future retries cache.set(f"idempotency:{key}", result, ex=3600) # Expire after 1 hour return jsonify(result), 200
Handling Duplicate Event Processing
When dealing with message brokers—which we introduced in our lesson on handling background tasks—the "at-least-once" delivery guarantee means duplicates are guaranteed.
To handle this, the consumer must be idempotent:
- Database Unique Constraints: If you are inserting records, rely on a
UNIQUEconstraint on the business key (e.g.,transaction_id). The database will reject a duplicate insertion, which you can catch and gracefully ignore. - State Machines: Ensure your entity state only moves forward. If an order is already
SHIPPED, a duplicateMARK_AS_SHIPPEDevent should be treated as a no-op rather than an error.
Hands-on Exercise
For our running project, we are currently designing the payment microservice.
- Update your API design document to include an
Idempotency-Keyrequirement for allPOSTandPATCHendpoints. - Sketch out a database schema change: add a table named
processed_requestswith columnsidempotency_key(PK) andresponse_payload. - Briefly explain how this table prevents the "double payment" scenario described above.
Common Pitfalls
- Ignoring Expiration: If you store idempotency keys forever, your database or cache will explode in size. Always set a TTL (Time-To-Live) on your idempotency keys.
- Non-Deterministic Keys: Never use a timestamp or a generic value as an idempotency key. Always use a UUID generated on the client side before the first attempt.
- Ignoring Concurrency: If two identical requests hit the server at the exact same time, both might see the cache as empty. Use a distributed lock or a database
INSERT ... ON CONFLICTto ensure atomic processing.
FAQ
Q: Do I need idempotency for GET requests?
A: No. HTTP GET requests are idempotent by definition—they should not have side effects.
Q: How long should I store idempotency keys? A: Typically 24 hours is sufficient, as most retry logic happens within minutes.
Q: Is idempotency the same as transactionality? A: No, but they are cousins. Transactionality ensures the operation is atomic; idempotency ensures the operation is safe to repeat.
Recap
Achieving idempotency is the difference between a system that breaks under pressure and one that gracefully handles network flakiness. By enforcing unique request IDs and utilizing database constraints, you ensure that your system remains consistent regardless of how many times a client retries an operation. Implementing these patterns, as discussed in REST API Idempotency and the Redis Idempotency Pattern, is a fundamental skill for building professional, reliable distributed systems.
Up next: We will explore Service Discovery to help our microservices find each other in a dynamic environment.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.
