Hello! In our last lesson, we navigated the complexities of consistency in distributed systems, establishing when to prioritize strong correctness versus eventual availability. We concluded by highlighting a critical challenge that arises from network uncertainty: when a client retries a request, how do we prevent an operation, like a payment, from executing multiple times?
This lesson provides the definitive answer to that question. We will explore the concept of idempotency, a fundamental property for building reliable distributed systems. Your goal for this lesson is to learn how to design idempotent API endpoints that can safely handle duplicate requests, a common occurrence during network retries. This is an essential skill for any backend engineer building robust services.
What is Idempotency?
In simple terms, an operation is idempotent if performing it multiple times has the same effect as performing it once. Think of pressing an elevator button: the first press calls the elevator, and subsequent presses do nothing new. The final state (the elevator being called) is the same.
Let's watch a short video that introduces this concept and why it's so critical in payment systems.
Idempotency and Intelligent Retry | PayPal
The ByteMonk channel provides a great analogy and context for idempotency using PayPal as an example.
First, watch the opening segment for a clear definition and analogy. Then, watch the explanation of why it's crucial in distributed systems like payment processing.
As you saw, without idempotency, a simple network timeout could cause a customer to be charged multiple times.
In the context of REST APIs, some HTTP methods are naturally idempotent, while others are not. Understanding this distinction is key.
Idempotency - What it is and How to Implement it
This video from Alex Hyett offers a precise breakdown of idempotency for common HTTP methods.
Watch the section that analyzes each HTTP method (GET, PUT, POST, PATCH, DELETE). Pay close attention to why POST is generally not idempotent, as this is the method we most often need to make safe for retries.
To summarize the key points from the video:
| Method | Idempotent? | Reason |
|---|---|---|
GET |
Yes | It's a read-only operation; it doesn't change the server's state. |
PUT |
Yes | It updates a resource to a specific state. Repeating the update results in the same final state. |
DELETE |
Yes | The first request deletes the resource. Subsequent requests find the resource already gone. Same state. |
POST |
No | It's used to create a new resource. Repeating a POST request typically creates multiple new resources. |
PATCH |
No | It applies a partial modification (e.g., incrementing a value). Repeating it changes the state each time. |
Our main challenge is with non-idempotent methods like POST. How can we make an operation like POST /orders safe to retry? The answer lies in a powerful pattern: the idempotency key.
The Idempotency Key Pattern
The core idea is simple: the client generates a unique identifier for each distinct operation and sends it in a request header, commonly named Idempotency-Key. The server then uses this key to recognize and de-duplicate retried requests for the same operation.
Payment gateways like Stripe and PayPal have standardized this pattern in their APIs. Let's see how PayPal uses it.
Idempotency and Intelligent Retry | PayPal
Returning to the ByteMonk video, let's look at the practical implementation of this pattern.
Watch the segment explaining what idempotency keys are and how they're used. Then, observe the simplified processing flow at PayPal, which shows how the key is checked before processing a payment.
The server-side logic follows these steps:
- A client initiates an operation (e.g., creating a payment) and generates a unique
Idempotency-Key(e.g., a UUID). - The server receives the request and extracts the key.
- The server checks a storage system to see if this key has been processed before.
- If the key is found: The server knows this is a retry. It doesn't re-process the operation. Instead, it retrieves the stored result from the original request and sends it back to the client.
- If the key is not found: This is a new operation. The server processes it, saves the result (the response body and status code) associated with the idempotency key, and then sends the response to the client.
This architecture ensures that even if the client retries the request ten times, the payment is only processed once.

Architecting an Idempotent Endpoint
To implement this, we need a storage system for the idempotency keys that is shared across all instances of our service. An in-memory dictionary won't work in a distributed environment with multiple server instances behind a load balancer.
A distributed cache like Redis is a perfect fit for this job. Let's explore the key architectural considerations.
Idempotency - What it is and How to Implement it
Let's go back to the Alex Hyett video for a deeper dive into the implementation details.
Watch the segment on implementing idempotency. Focus on: Why an external key-value store like Redis is necessary. The importance of setting a Time-To-Live (TTL) on the keys. The flow for handling new vs. existing keys.
As the video highlighted, setting a TTL (e.g., 24-48 hours) is crucial. It prevents the key store from growing indefinitely and strikes a balance between safety and resource usage.
The following diagram illustrates the complete server-side logic, including how to handle concurrent requests for the same key.
This resource provides a clear architectural diagram and explanation of the flow.
Please read the "Architecture" section. Pay close attention to the three possible paths when checking the idempotency store: Key exists, completed: Return the cached response. Key exists, in-progress: Another request is already being processed. Return a 409 Conflict. Key does not exist: Proceed with processing.
Implementation with FastAPI and Redis
Now let's translate this architecture into code. Given your background with FastAPI, a natural way to implement this is with middleware, which can intercept incoming requests and apply the idempotency logic automatically.
The resource we just reviewed provides an excellent Python implementation of this pattern. We will walk through it piece by piece.
Idempotency Store and FastAPI Middleware
This resource contains a production-ready implementation of the idempotency pattern.
Study the two Python code blocks: The IdempotencyStore class under the heading "Idempotency Store". The IdempotencyMiddleware class under "FastAPI Middleware". As you read, try to map the code back to the architectural flow we just discussed.
Let's break down the key parts of that implementation.
1. The IdempotencyStore
This class abstracts the interaction with Redis.
# From resource [LINK](https://lobehub.com/skills/dadbodgeoff-drift-idempotency)
class IdempotencyStore:
# ... (initialization) ...
def get(self, key: str) -> Optional[IdempotencyRecord]:
# Fetches the record from Redis.
# ...
def acquire_lock(self, key: str) -> bool:
# Tries to set the key with status 'processing'.
# The 'nx=True' flag makes this an atomic "set if not exists" operation.
# This is how we handle concurrent requests safely.
result = self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.lock_ttl,
nx=True,
)
return result is True
def complete(self, key: str, response: Dict[str, Any]) -> None:
# Overwrites the key with the final 'completed' status and response.
# Sets the final TTL for the response.
# ...
def release(self, key: str) -> None:
# Deletes the key, used if an error occurs during processing.
# ...
The acquire_lock method is critical. It uses Redis's atomic SET ... NX command to ensure that only one server instance can acquire a lock for a given idempotency key at a time.
2. The IdempotencyMiddleware
This FastAPI middleware ties everything together. Here's a simplified view of its dispatch method, which contains the core logic:
# From resource [LINK](https://lobehub.com/skills/dadbodgeoff-drift-idempotency)
async def dispatch(self, request: Request, call_next):
# ... (check for idempotency key in header) ...
# 1. Check for an existing record
existing = self.store.get(full_key)
if existing:
if existing.status == "processing":
# Another request is in-flight, so we reject this one.
raise HTTPException(status_code=409, detail="Request is being processed")
if existing.status == "completed":
# The request was already completed, so we replay the stored response.
return JSONResponse(content=existing.response["body"], ...)
# 2. Acquire a lock for the new request
if not self.store.acquire_lock(full_key):
raise HTTPException(status_code=409, detail="Request is being processed")
# 3. Process the request and store the result
try:
response = await call_next(request) # This calls the actual API endpoint
# On success, read the response body and cache it
body = ...
self.store.complete(full_key, {"status_code": response.status_code, "body": json.loads(body)})
return JSONResponse(...)
except Exception:
# If an error occurs, release the lock so the client can retry.
self.store.release(full_key)
raise
This middleware provides a powerful, reusable pattern that you can apply to your POST, PUT, or PATCH endpoints to make them fully idempotent.
System Design Thinking
Designing an idempotent system involves several trade-offs. Consider these questions:
- Key Generation: Who should be responsible for generating the idempotency key? Should a client generate a new key for every single retry attempt, or use the same key for all retries of a single logical operation?
Answer
The client must use the **same key** for all retries of a single logical operation. If it generated a new key for each retry, the server would treat each one as a new, unique request, defeating the purpose of idempotency.
2. **TTL Management:** The code example uses a response TTL of 24 hours. What are the trade-offs of using a very short TTL (e.g., 5 minutes) versus a very long one (e.g., 7 days)?
Answer
- **Short TTL (5 minutes):** Pro: Reduces storage requirements in Redis. Con: If the client's retry mechanism has a long backoff period, a retry might occur after the key has expired, leading to a duplicate operation. - **Long TTL (7 days):** Pro: Provides a much larger safety window against duplicates. Con: Uses significantly more storage. It could also prevent a user from intentionally performing the same action again after a few days if they happen to reuse a key (though this is unlikely with UUIDs). The 24-48 hour range is a common industry compromise.3. **Error Caching:** The provided middleware only caches successful responses (`200 <= status_code < 500`). It releases the lock on a `5xx` server error. Why is this a good design choice? What would be the risk of caching a `500 Internal Server Error` response?
Answer
A `5xx` error often indicates a transient problem on the server (e.g., a temporary database outage). If we cached and replayed the `500` error, any client retry would automatically fail without actually retrying the operation. This would prevent the system from recovering once the transient issue is resolved. By releasing the lock, we allow the client's next retry attempt to execute the operation again, which might succeed if the server has recovered.Conclusion
Today, we've tackled one of the most common and critical problems in distributed systems: ensuring that operations are not accidentally duplicated due to network retries. By designing idempotent APIs, you build a layer of safety and reliability into your services.
Key Takeaways:
- Idempotency is the property where an operation can be executed multiple times with the same result as executing it once.
- While
GET,PUT, andDELETEare naturally idempotent,POSTandPATCHare not and require special handling. - The Idempotency Key Pattern is the standard solution, where a client-sent key is used by the server to de-duplicate requests.
- A robust implementation requires a shared, persistent store like Redis to track keys and responses across all service instances.
- The architecture must handle three states: completed requests (replay response), in-progress requests (reject with
409 Conflict), and new requests (process and store).
Preview of the Next Lesson:
We have now made our API endpoints safe to be retried. The logical next step is to look at the other side of the equation: the client. In the next lesson, we will learn how to implement a smart retry strategy in a Python client, using techniques like exponential backoff to handle transient failures gracefully without overwhelming the server.