In distributed systems, the same request is often sent more than once. A mobile application may retry after a timeout, an API gateway may automatically retry failed requests, or a message broker may redeliver a message after a consumer crash. Without proper safeguards, these duplicate requests can create duplicate orders, process multiple payments, send repeated emails, or corrupt business data.
This is where idempotency becomes essential. An idempotent API guarantees that processing the same request multiple times produces the same result as processing it once. Rather than relying on clients to avoid retries, the server safely handles duplicate requests while maintaining data consistency.
This article demonstrates how to build production-ready idempotent ASP.NET Core APIs using the Idempotency-Key pattern, distributed caching, and transactional persistence.
Why Duplicate Requests Happen
Duplicate requests are normal in distributed systems and should be expected.
Common scenarios include:
HTTP client retries after timeout
Browser refresh during form submission
API Gateway retry policies
Reverse proxy retries
Mobile network instability
Message broker redelivery
User clicking the Submit button multiple times
Consider an order creation endpoint:
POST /api/orders
If the client retries because the response was lost, the server may create two orders even though the customer intended to create only one.
The problem is rarely visible during development but becomes common in production.
Idempotency vs Retry
These concepts are related but different.
| Retry | Idempotency |
|---|---|
| Client sends request again | Server safely handles duplicate requests |
| Improves reliability | Prevents duplicate processing |
| Client responsibility | Server responsibility |
| May execute operation multiple times | Guarantees only one successful execution |
Retries without idempotency increase the likelihood of duplicate data.
Typical Request Flow
sequenceDiagram
participant Client
participant API
participant Cache
participant Database
Client->>API: POST /orders + Idempotency-Key
API->>Cache: Check Key
alt Key Exists
Cache-->>API: Previous Response
API-->>Client: Return Cached Result
else New Request
API->>Database: Save Order
API->>Cache: Store Response
API-->>Client: Success
end
The API never processes the same key twice.
Choosing an Idempotency Key
Clients generate a unique identifier for every logical request.
Example:
Idempotency-Key:
9f4a8ef5-5d0d-4fd8-a94f-c58ab7ef41b3
Good choices include:
GUID
UUID
Secure random identifier
Avoid:
Timestamp
Username
Order Number
Email Address
The key should identify a single business operation—not a user.
Designing the API Contract
POST /api/orders
Headers
Idempotency-Key:
9f4a8ef5-5d0d-4fd8-a94f-c58ab7ef41b3
If the same request arrives again with the same key:
Do not create another order.
Return the original response.
Clients receive identical results regardless of how many retries occur.
Building an Idempotency Middleware
Instead of duplicating logic inside every controller, centralize it.
public class IdempotencyMiddleware
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;
public IdempotencyMiddleware(
RequestDelegate next,
IDistributedCache cache)
{
_next = next;
_cache = cache;
}
public async Task Invoke(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(
"Idempotency-Key",
out var key))
{
await _next(context);
return;
}
var cachedResponse =
await _cache.GetStringAsync(key!);
if (cachedResponse != null)
{
context.Response.ContentType =
"application/json";
await context.Response.WriteAsync(
cachedResponse);
return;
}
var originalBody = context.Response.Body;
using var memoryStream =
new MemoryStream();
context.Response.Body = memoryStream;
await _next(context);
memoryStream.Position = 0;
var response =
await new StreamReader(memoryStream)
.ReadToEndAsync();
await _cache.SetStringAsync(
key!,
response,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromHours(24)
});
memoryStream.Position = 0;
await memoryStream.CopyToAsync(originalBody);
context.Response.Body = originalBody;
}
}
This middleware intercepts duplicate requests before controller logic executes.
Registering the Middleware
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration
.GetConnectionString("Redis");
});
app.UseMiddleware<IdempotencyMiddleware>();
Distributed caching allows every API instance to share the same idempotency store.
Why In-Memory Cache Is Not Enough
Many developers begin with:

Jasen FiciPosted Aug 6, 2026, 12:54 PM
We included this article in DotNetNews here: https://dotnetnews.co/archive/the-net-news-daily-issue-513/