Opening the original article.
Opening original article…
You’ll be redirected to the original source shortly.
Introduction
When building APIs, one of the most common challenges is dealing with duplicate requests. Imagine a user submitting an order, completing a payment, or triggering an important business operation. The request reaches the server and is processed successfully, but due to a network issue or timeout, the client never receives the response. Believing that the operation failed, the user retries the same request.
Without proper protection, the API may process the operation multiple times. This can lead to duplicate orders, double payments, repeated emails, or the same workflow being executed more than once. These situations are particularly common in distributed systems, where retries are often performed automatically by clients, gateways, or background services.
To solve this problem, many modern applications implement the Idempotency Pattern. The idea is simple: if the same request is sent multiple times, the server should process it only once and return the same result for subsequent attempts. This makes APIs more reliable, prevents unintended side effects, and allows clients to safely retry requests when failures occur.
In this article, you’ll learn what idempotency is, why it matters, and how to implement the Idempotency Pattern in ASP.NET Core. We’ll start with the basic concepts and then build a practical Minimal API example that prevents duplicate requests using an idempotency key.
What Is Idempotency and Why It Matters
In software development, idempotency is the property of an operation that can be performed multiple times while producing the same result. In other words, executing the same request once or several times should leave the system in the same final state.
A simple real-world example is pressing the elevator button. Pressing the button once requests the elevator, and pressing it several more times does not create additional elevators or change the final outcome. The action has the same effect regardless of how many times it is repeated.
The concept of idempotency is widely used in computer science and web development because it helps systems remain reliable when failures occur. If you’d like to learn more about the general concept, you can find a detailed explanation on Wikipedia: https://en.wikipedia.org/wiki/Idempotence
In the context of APIs, idempotency becomes especially important when dealing with operations that create or modify data. Consider an API endpoint that creates a new order. If the client sends the same request twice due to a timeout or network interruption, the server could accidentally create two orders instead of one. Similar problems can happen with payments, email notifications, inventory updates, and webhook processing.
By implementing idempotency, the server can recognize that a request has already been processed and return the original response instead of executing the operation again. This prevents duplicate actions and makes the API safer and more predictable.
Some of the main benefits of implementing idempotency include:
Preventing duplicate orders and payments
Handling network failures and retries safely
Improving reliability in distributed systems
Reducing the risk of inconsistent data
Providing a better experience for API consumers
As applications become more distributed and interconnected, idempotency is no longer just a nice-to-have feature. For many business-critical operations, it is an essential part of building robust and reliable APIs.

How to Implement the Idempotency Pattern – Duplicate Requests
Understanding Idempotent and Non-Idempotent HTTP Methods
HTTP already defines some methods as idempotent by design. This means that sending the same request multiple times should have the same effect on the server as sending it once.
The following table summarizes the most common HTTP methods and their idempotency characteristics:
HTTP Method | Idempotent | Description |
|---|---|---|
GET | Yes | Retrieves data without modifying the server state. |
HEAD | Yes | Retrieves metadata about a resource. |
PUT | Yes | Creates or replaces a resource with the same final result. |
DELETE | Yes | Removes a resource. Repeating the request does not change the final state. |
OPTIONS | Yes | Returns information about supported operations. |
POST | No | Typically creates new resources or triggers actions. |
PATCH | No | Applies partial updates that may change the resource each time. |
For example, a GET request that retrieves a product can be executed multiple times without changing any data. Similarly, a PUT request that updates a user’s email address will always result in the same final state, regardless of how many times the request is repeated with the same payload.
The situation is different for POST requests. Since POST is commonly used to create resources or execute business operations, repeating the same request can lead to duplicate records or unintended side effects.
Consider the following endpoint:
POST /api/ordersIf the request is processed twice, the API might create two separate orders instead of one. The same issue can occur with payment processing, subscription creation, email notifications, and webhook handling.
This is why most idempotency implementations focus on POST endpoints. By introducing an idempotency key, the server can detect duplicate requests and ensure that the operation is executed only once, even if the client retries the request multiple times.
How the Idempotency Pattern Works
The most common way to implement idempotency is by using an idempotency key. An idempotency key is a unique identifier generated by the client and sent with the request. The server uses this key to determine whether the operation has already been processed.
A typical request might look like this:
POST /api/orders
Idempotency-Key: 2f44f20f-97a4-4ea9-a33d-8d5d8f5e6f74
Content-Type: application/json
{
"productName": "Laptop",
"quantity": 1
}When the server receives the request, it first checks whether the provided idempotency key has already been used.
If the key is new, the server processes the request normally, stores the result together with the idempotency key, and returns the response to the client.
If the same request is received again with the same idempotency key, the server does not execute the business operation a second time. Instead, it returns the previously stored response.
The workflow can be visualized as follows:

Idempotency Pattern
This approach ensures that retries are safe. Even if a request is accidentally sent multiple times due to network failures, browser refreshes, automatic retry policies, or webhook redelivery mechanisms, the operation will only be executed once.
Let’s consider a simple order creation scenario. A client sends a request to create an order using the idempotency key ABC123. The server creates the order and stores the response associated with that key. A few seconds later, the client retries the same request because it never received the original response. Since the server recognizes the key, it simply returns the existing result instead of creating a second order.
One important detail is that the idempotency key must uniquely identify a business operation. Reusing the same key for different requests can lead to unexpected behavior. For this reason, many systems also store a hash of the request payload and verify that repeated requests contain the same data before returning a cached response.
Practical Example: Building an Idempotent ASP.NET Core Minimal API
Disclaimer: This example is purely for educational purposes. There are better ways to structure and optimize real-world applications. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.
In this example, we will build a simple ASP.NET Core Minimal API that creates orders and prevents duplicate requests using the Idempotency Pattern.
The application will include:
CreateOrderRequest, Represents the request sent by the client to create a new order.
OrderResponse, Represents the response returned by the API after an order is created.
Idempotency-Key, A unique value sent by the client in the request header to identify the operation.
In-memory storage, Used to store processed idempotency keys and their responses for the purpose of this demo.
The goal is to understand how the Idempotency Pattern can help an API safely handle retries without creating duplicate orders.
Prerequisites
Before starting, make sure you have the following installed:
.NET SDK: Download and install the .NET SDK if you haven’t already.
Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
C# Dev Kit for VSCode: Install the C# Dev Kit for VSCode to enable C# support.
Verify Your .NET Installation
Before creating the project, make sure the .NET SDK is installed.
Open your terminal and run:
dotnet --versionIf everything is installed correctly, you should see the installed .NET version.
Step 1 – Create a Minimal API Project
Open your terminal and create a new ASP.NET Core Minimal API project using the .NET CLI:
dotnet new web -n IdempotencyDemo
cd IdempotencyDemo
How to Implement the Idempotency Pattern – Create Project
This creates a simple ASP.NET Core project with a minimal Program.cs file.
Next, open the project in Visual Studio Code:
code .You should now see the default project structure with the Program.cs file ready to be modified.
Step 2 – Create the Request and Response Models
For this example, we will use two simple records: one for the incoming request and one for the response returned by the API.
Add the following code to Program.cs:
public record CreateOrderRequest(
string ProductName,
int Quantity);
public record OrderResponse(
Guid OrderId,
string ProductName,
int Quantity,
DateTime CreatedAt);CreateOrderRequest represents the data sent by the client when creating an order. OrderResponse represents the result returned by the API after the order has been created.

How to Implement the Idempotency Pattern – Records
Step 3 – Implement the Idempotency Storage
To keep the example simple, we’ll use an in-memory dictionary to store processed requests and their responses.
In a real-world application, you would typically use a distributed cache such as Redis or persist the data in a database. We’ll discuss production-ready options later in this article.
Add the following code below the model definitions:
var processedRequests = new Dictionary<string, OrderResponse>();The dictionary key represents the Idempotency-Key received from the client, while the value contains the response previously generated by the API.
Step 4 – Create the Idempotent Endpoint
Now let’s create an endpoint that uses the idempotency key to prevent duplicate order creation.
Replace the contents of Program.cs with the following endpoint:
app.MapPost("/api/orders",
(
CreateOrderRequest request,
HttpContext context
) =>
{
var idempotencyKey =
context.Request.Headers["Idempotency-Key"]
.FirstOrDefault();
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return Results.BadRequest(
"Missing Idempotency-Key header.");
}
if (processedRequests.TryGetValue(
idempotencyKey,
out var existingOrder))
{
return Results.Ok(existingOrder);
}
var order = new OrderResponse(
Guid.NewGuid(),
request.ProductName,
request.Quantity,
DateTime.UtcNow);
processedRequests[idempotencyKey] = order;
return Results.Created(
$"/api/orders/{order.OrderId}",
order);
});The endpoint first checks whether the request contains an Idempotency-Key header.
If the key is missing, the API returns a 400 Bad Request response.
If the key already exists in our dictionary, the API returns the previously stored response without creating a new order.
If the key has never been seen before, a new order is created, stored in memory, and returned to the client.
This ensures that the same operation is executed only once, even if the client retries the request multiple times.
Complete Program.cs
At this point, your complete Program.cs file should look like this:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var processedRequests =
new Dictionary<string, OrderResponse>();
app.MapPost("/api/orders",
(
CreateOrderRequest request,
HttpContext context
) =>
{
var idempotencyKey =
context.Request.Headers["Idempotency-Key"]
.FirstOrDefault();
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return Results.BadRequest(
"Missing Idempotency-Key header.");
}
if (processedRequests.TryGetValue(
idempotencyKey,
out var existingOrder))
{
return Results.Ok(existingOrder);
}
var order = new OrderResponse(
Guid.NewGuid(),
request.ProductName,
request.Quantity,
DateTime.UtcNow);
processedRequests[idempotencyKey] = order;
return Results.Created(
$"/api/orders/{order.OrderId}",
order);
});
app.Run();
public record CreateOrderRequest(
string ProductName,
int Quantity);
public record OrderResponse(
Guid OrderId,
string ProductName,
int Quantity,
DateTime CreatedAt);
How to Implement the Idempotency Pattern – Test API
Testing the API
Let’s verify that our API correctly handles duplicate requests.
Start the application:
dotnet runThen create a file named test.http and add the following requests:
@HostAddress = http://localhost:5000
### First Request
POST {{HostAddress}}/api/orders
Idempotency-Key: order-123
Content-Type: application/json
{
"productName": "Laptop",
"quantity": 1
}
### Same Request Again
POST {{HostAddress}}/api/orders
Idempotency-Key: order-123
Content-Type: application/json
{
"productName": "Laptop",
"quantity": 1
}
How to Implement the Idempotency Pattern – Test File
The first request creates a new order and returns a response similar to the following:
{
"orderId": "8c9b1b0c-3ef9-4c4e-a2d9-7e8f9c4d1234",
"productName": "Laptop",
"quantity": 1,
"createdAt": "2026-06-12T10:00:00Z"
}
How to Implement the Idempotency Pattern – Test API
When the second request is executed with the same Idempotency-Key, the API does not create a new order. Instead, it returns the previously stored response.
Notice that the orderId remains exactly the same. This confirms that the operation was processed only once, even though the request was sent twice.
This simple mechanism allows clients to safely retry requests without the risk of creating duplicate orders or triggering the same business operation multiple times.
Moving to Production
The example shown in this article is intentionally simple so that the core concept is easy to understand. While using an in-memory dictionary works well for learning purposes, it has several limitations that make it unsuitable for production environments.
First, all data is lost whenever the application restarts. If the server goes down or a new deployment is performed, every stored idempotency key disappears and duplicate requests can be processed again.
Second, the solution does not work correctly in a distributed environment. Modern applications are often deployed across multiple instances behind a load balancer. If the first request is handled by one server and the retry is routed to another server, each instance will have its own dictionary and duplicate operations may still occur.
For production systems, a shared storage mechanism should be used instead. Common options include:
Redis
SQL Server
PostgreSQL
IDistributedCache implementations
For example, ASP.NET Core provides the IDistributedCache abstraction, allowing you to store idempotency data in a shared location that can be accessed by all application instances.
Another important consideration is key expiration. Idempotency keys should not be stored forever because they consume storage and become less useful over time. Most systems define an expiration period ranging from a few minutes to several hours, depending on the business requirements.
A common approach is:
Payments: 24 to 48 hours
Orders: several hours
Webhooks: a few hours
Background jobs: based on retry policies
Finally, consider what happens if a client accidentally reuses the same idempotency key for a different request.
For example:
Request 1:
{
"productName": "Laptop",
"quantity": 1
}Request 2:
{
"productName": "Monitor",
"quantity": 2
}If both requests use the same idempotency key, returning the original response would be incorrect.
To prevent this situation, many production systems store a hash of the request payload together with the idempotency key. When a duplicate request arrives, the server compares the payload hash with the original one. If they differ, the API returns an error instead of reusing the stored response.
By combining shared storage, expiration policies, and payload validation, you can build a robust idempotency solution that works reliably even in large-scale distributed systems.
Best Practices and Common Pitfalls
Implementing idempotency is conceptually simple, but there are several details that can impact the reliability of your solution. Here are some best practices and common mistakes to keep in mind when designing idempotent APIs.
Use a Unique Idempotency Key Per Operation
An idempotency key should represent a single business operation. Clients should generate a new key whenever they perform a new action.
For example:
Create Order #1 -> Key A
Create Order #2 -> Key BReusing the same key for different operations can lead to unexpected behavior and incorrect responses.
Set an Expiration Time
Idempotency keys should not be stored forever. Over time, they consume storage and become less valuable.
Choose an expiration period based on your business requirements:
Payments: 24-48 hours
Orders: a few hours
Webhooks: a few hours
Background jobs: based on retry policies
This helps keep storage requirements under control while still covering typical retry scenarios.
Validate the Request Payload
A common mistake is to trust the idempotency key alone.
Consider the following situation:
Idempotency-Key: order-123First request:
{
"productName": "Laptop",
"quantity": 1
}Second request:
{
"productName": "Monitor",
"quantity": 2
}Although the key is identical, the payload is completely different.
To avoid inconsistencies, many systems store a hash of the original request and compare it with subsequent requests using the same key.
Consider Concurrency
In high-traffic systems, two identical requests may arrive at nearly the same time.
A naive implementation can produce the following sequence:
Request A -> Key not found
Request B -> Key not found
Request A -> Create order
Request B -> Create orderThe result is two orders instead of one.
Production systems often use distributed locks, database constraints, or atomic operations to prevent this race condition.
Use Shared Storage in Distributed Systems
If your application runs on multiple servers, storing idempotency keys in memory is not enough.
Instead, use a shared storage solution such as:
Redis
SQL Server
PostgreSQL
Distributed Cache
This ensures all application instances can access the same idempotency information.
Don’t Apply Idempotency Everywhere
Not every endpoint requires idempotency.
Read-only operations such as GET requests are already idempotent by nature and typically don’t need additional protection.
Focus your efforts on operations that:
Create resources
Process payments
Trigger workflows
Send notifications
Handle webhooks
Applying idempotency selectively keeps your implementation simpler and easier to maintain.
When Should You Use Idempotency?
Idempotency is not required for every API endpoint, but it becomes extremely valuable whenever an operation creates, modifies, or triggers something important.
As a general rule, if executing the same request multiple times could cause unwanted side effects, idempotency should be considered.
Common use cases include:
Payment Processing
Payment APIs are one of the most common examples. If a client retries a payment request due to a timeout or network failure, the customer should not be charged twice.
Order Creation
E-commerce systems often allow users to place orders through POST endpoints. Without idempotency, duplicate requests can result in multiple orders being created for the same purchase.
Webhook Processing
Many external services automatically retry webhook deliveries when they do not receive a successful response. Idempotency ensures that the same event is processed only once.
Background Jobs
Message queues and background processing systems may redeliver messages when failures occur. Idempotent operations help prevent duplicate processing and inconsistent data.
External Integrations
When communicating with third-party systems, retries are often unavoidable. Using idempotency helps ensure that repeated requests do not create duplicate records or trigger the same action multiple times.
In contrast, idempotency is usually unnecessary for read-only operations such as GET requests, since they are already idempotent by design and do not modify application state.
As your applications grow and become more distributed, implementing idempotency for critical operations becomes an important part of building reliable and resilient APIs.
Conclusion
Handling duplicate requests is a common challenge when building modern APIs. Network failures, client retries, webhook redeliveries, and temporary outages can all cause the same request to be sent multiple times.
Without proper safeguards, these duplicate requests can lead to unwanted side effects such as duplicate orders, double payments, repeated notifications, or inconsistent data.
The Idempotency Pattern provides a simple and effective solution. By associating each operation with a unique idempotency key, the server can detect previously processed requests and safely return the original response instead of executing the operation again.
In this article, we’ve explored the fundamentals of idempotency, discussed when it should be used, and built a practical ASP.NET Core Minimal API example that prevents duplicate order creation. While the demo used an in-memory dictionary for simplicity, production systems typically rely on shared storage solutions such as Redis, SQL Server, or distributed caches to ensure consistency across multiple application instances.
By implementing idempotency in the right places, you can make your APIs more reliable, more resilient to failures, and safer for the applications and users that depend on them.
If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.
Thanks for reading!