October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk12 min

The Retry Storm Problem: Why Your ASP.NET Core API Needs Idempotency Keys

Retry policies limit how often clients repeat requests while a dependency is struggling. Idempotency keys stop a retried POST from taking effect twice. This guide explains how the two differ and how to design the server side in ASP.NET Core.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A retry policy and an idempotency key solve different problems, and an endpoint that creates orders, charges cards, or sends messages usually needs both. A retry policy controls how often a client repeats a request while a dependency is unhealthy, which protects that dependency from a flood of repeats. An idempotency key lets your API recognize that a repeated request is the same logical operation, so its effect is applied once. Neither replaces the other. A key does not reduce retry volume, and a bounded retry policy does not make a non-idempotent POST safe to repeat.

Two controls, two failure modes

Retry handling and idempotency handling are often discussed together because both show up when a request fails. They answer different questions, and each leaves the other’s problem unsolved.

As an Amazon Associate I earn from qualifying purchases.

Question Retry policy Idempotency handling
Problem it addresses Repeated attempts overwhelm a struggling dependency A repeated state-changing request applies its effect twice
Where it usually lives Client side, in the outbound call path (for .NET, the HttpClient resilience handler) Server side, in the endpoint or a layer in front of it
What it controls Attempt count, delay between attempts, which failures qualify, and when to stop Whether a request with the same key and the same content performs the work again
What it cannot do Make a non-idempotent operation safe to repeat Reduce the number of attempts a client makes
Typical failure it prevents A retry storm during an outage A duplicate order after a timeout where the server had already committed the first one

The timeout the server already completed

When a client times out, it cannot tell which of several things happened on the server. Four states are possible:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. The request never reached the server. Repeating it is safe.
  2. The server failed before making any change. Repeating is usually safe, provided the failed attempt left nothing behind.
  3. The server committed the change, but the response was lost on the way back. Repeating without a key creates a second order.
  4. The server is still working when the client gives up. A repeat can start a second execution while the first is still running.

Only the first case is safe to repeat without help. The other three require the server to recognize the repeat. That is what an idempotency key provides: the client attaches one identifier to a logical operation, reuses it on every attempt, and the server returns the stored outcome instead of doing the work again.

What a retry storm looks like

Microsoft’s Azure Architecture Center describes the Retry Storm antipattern in its pattern guidance. The core statement is: “When a service becomes unavailable or busy, frequent client retries can prevent the service from recovering and worsen the problem.” The guidance attributes the problem to frequent or indefinite retries during unavailability or overload, when clients keep pushing work at a service that cannot absorb it.

Symptoms

  • Request volume to a dependency rises during an incident, and error rates stay high after the dependency has started to recover.
  • Clients retry on the same schedule, so traffic arrives in synchronized waves instead of a smooth curve.
  • One user action fans out into many downstream calls because several layers retry on their own.

Controls that limit retry volume

  • Cap attempts and total duration. A limit on count alone can still allow very long waits, so cap both.
  • Back off between attempts. Exponential backoff increases the wait after each failure, so the retry rate falls as failures persist.
  • Add jitter. Randomizing each client’s delay keeps clients that failed together from retrying together. Randomness is not a complete fix: Stripe’s engineering writing on retries notes that backoff schedules can still line up and overload a recovering server. Watch aggregate request rates, not only per-client delays.
  • Open a circuit breaker. After repeated failures, stop calls to the dependency for a period rather than sending each request into a failing path.
  • Honor Retry-After. When the server states when to come back, that value is better information than your own schedule.
  • Do not retry permanent client errors. A 400 Bad Request will usually fail the same way on every attempt.
  • Prefer maintained SDK retry policies where they fit, and read their defaults before adding another retry layer on top.

What the .NET resilience handler does and does not do

Microsoft’s .NET HTTP resilience documentation covers outbound calls made through HttpClient. It governs how your application retries calls it makes to other services. It does not make your ASP.NET Core endpoints deduplicate incoming requests. On the outbound side, the standard handler retries a defined set of transient outcomes:

Outcome Retried by the standard handler
HTTP 500 and above Yes, as a transient response class
HTTP 408 Request Timeout Yes
HTTP 429 Too Many Requests Yes
HttpRequestException Yes, as a transient exception
TimeoutRejectedException Yes, as a transient exception
HTTP 400 Bad Request No. It is not among the listed transient classes

The documented standard defaults

Microsoft documents the standard retry strategy as three retries, exponential backoff, jitter enabled, and a two-second delay. These values are version-sensitive. Confirm them in the documentation for the package version your project references before you rely on them. They describe the standard handler’s defaults and are not a guarantee for every HttpClient configuration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Turning off retries for unsafe methods

Unless you change it, the standard retry applies to POST as well. If an endpoint is a non-idempotent POST and you have not yet added server-side deduplication, the safer default is to stop the handler from repeating it. Microsoft’s documentation shows two controls for this, DisableForUnsafeHttpMethods() and DisableFor(HttpMethod.Post, HttpMethod.Delete). A configuration sketch:

builder.Services
    .AddHttpClient("OrdersApi", client =>
    {
        client.BaseAddress = new Uri("https://orders.internal/");
    })
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.DisableForUnsafeHttpMethods();
    });

Check the method names against the version of the resilience package you install. The trade-off is availability. A transient failure on a POST now reaches your calling code, which must decide what to do. Once a server-side key exists, you can take control explicitly: generate the key once per logical operation, reuse it on each attempt, and retry only the failures you have classified as transient.

Choosing a header convention

Two header conventions appear in current guidance, and they are not interchangeable. Stripe’s API uses Idempotency-Key. Microsoft’s Azure API guidelines define Repeatability headers for POST operations.

Axis Stripe-style Idempotency-Key Azure Repeatability headers
Headers Idempotency-Key Repeatability-First-Sent and Repeatability-Request-ID, with Repeatability-Result discussed in the same guidelines
Key length Maximum 255 characters, as documented in Stripe’s API reference Not stated in the Azure guidance reviewed
Retention Stripe prunes keys automatically once they are at least 24 hours old. This is Stripe’s own behavior, not an industry standard The tracked window must be at least five minutes. This is a minimum, not a fixed expiry
Replay Saves the resulting status and body once endpoint execution begins, and repeats them on retry The guidelines discuss returning the result of an earlier request; check the exact semantics in the guideline before implementing
Where it is defined Stripe’s API documentation Microsoft’s Azure API guidelines for POST operations

Pick one convention, document it in your API reference or OpenAPI description, and apply it to every POST that needs repeatability. Mixing both on one endpoint gives a single operation two identities, and the server then has to guess which one matters.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Designing the server side

ASP.NET Core does not recognize an Idempotency-Key header and deduplicate requests on its own. Inbound deduplication is a design you build and own. The decisions below need written answers before you write the code.

Key scope

Decide what a key identifies. In most APIs that means the tenant or account, the operation name, and the key value together. A raw key that is only unique across the whole system is not enough: two tenants can generate the same value, and without tenant scope one customer could replay another customer’s stored response. Scoping by operation also keeps a key used for CreateOrder from matching a different request type.

Request fingerprint

Bind each key to a fingerprint of the request, typically a hash of the route, the operation name, and a canonical form of the body. A repeat with the same key and a matching fingerprint gets the stored outcome. A repeat with the same key and a different fingerprint gets a conflict response and is not executed. Canonicalization matters. Differences in property order, whitespace, or defaults should not produce false conflicts, while fields that legitimately change between attempts must be excluded on purpose. Stripe compares request parameters on retries for this reason.

Atomic claim

A check-then-act pattern, where the handler looks up the key and inserts it only if it is absent, is racy. Two API instances can both see no record and both proceed. Make the claim a single atomic operation, normally an insert guarded by a unique constraint or an equivalent conditional write. The request that loses the race receives a duplicate error and reads the winner’s record. Microsoft’s implementation guidance supports tracking processed identifiers but does not prescribe a particular transaction pattern, so the mechanism is your design choice.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Concurrent duplicates

A duplicate that arrives while the original is still running must not run the mutation. Choose one behavior and document it:

  • Wait up to a bounded time for the original to finish, then replay its outcome.
  • Return a conflict response that says the operation is in progress and gives a point to check again.
  • Return a retryable status with a Retry-After value.

Plan for the original crashing mid-operation too. If the claim is held outside the business transaction, it needs a lease with an expiry so that it cannot block the key indefinitely. If the claim and the mutation commit together, a crash rolls both back, as shown in the flow below.

Outcome storage and failures

Decide what a replay returns. Stripe’s documented behavior saves the resulting status and body once endpoint execution begins and repeats them on retry, including 500 errors. That is one design choice, not a universal rule. If you store only successes, a retry after a failure runs the mutation again, which is dangerous when the failure came after a partial side effect. If you store failures too, a client that received a 500 will keep receiving it for that key and must use a new key to try again. Whichever you choose, document it.

Next, choose what the replay shape is:

Axis Replay the saved original response Return an operation or status resource
What the retry receives The same status code and body as the first completion A reference to the operation, which the client checks until it finishes
Client changes needed None beyond reusing the key Polling logic and handling of a separate status shape
Fits best Synchronous POSTs such as order creation or payment capture Long-running work where the first call returns early
Main risk The stored body can become stale relative to later changes An extra endpoint to secure and an expiry to manage

Retention

Set the retention period from the longest realistic retry window. That window includes the client’s full retry schedule and any retries delayed by an outage or queued in a worker. An expired key makes a late retry look like a new request, and that is where duplicates return. The published figures in the table above are one product’s choice and a standard’s minimum. Neither is guidance for your business. Keep a business-level uniqueness rule as a backstop, such as a unique client order reference or an external payment reference, so that expiry cannot silently duplicate an effect. Schedule deletion of expired records so that storage stays bounded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Side effects outside the database

A key record and a database write can commit in one transaction. An email, a call to a payment provider, or a published message cannot. If the external system accepts its own idempotency key, pass your logical key through. If it does not, record the intended side effect in the same transaction as the business write and deliver it from there. A transactional outbox is the usual pattern, with deduplication on the receiving side where possible. No single implementation fits every system, so choose the design that matches your downstream dependencies.

Observability

Count the events that show whether the design works: duplicate hits served from stored outcomes, in-progress collisions, key mismatches, requests against expired keys, retry attempts per logical operation, and circuit-breaker openings. Log a hash or truncated form of the key together with tenant and operation, not the raw key, unless your data policy allows raw keys in logs.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choosing where the records live

Process-local memory alone does not coordinate requests that are routed to different instances. Two instances each hold their own dictionary, so a retry that lands on the other instance executes again. The options below are shared-store choices. Microsoft’s implementation guidance names Azure Table Storage and Managed Redis as examples of shared storage for tracked identifiers. They are examples, not a recommended default.

Option Coordinates across instances Durability Trade-offs
In-process memory (dictionary or memory cache) No Lost on restart Simplest to build. Acceptable only for a single instance or for tests
Shared relational table with a unique constraint Yes, through the constraint Same as your primary database The key record and the business write can share one transaction. Adds load to the database you already run
Azure Table Storage (Microsoft’s example) Yes Managed by the provider Separate from your SQL data, so the record and business write cannot commit atomically. Check its conditional-write behavior
Managed Redis (Microsoft’s example) Yes Depends on persistence and eviction settings Fast expiry suits short retention. Confirm persistence and eviction policy before relying on it for correctness

A minimal claim-and-replay flow

The schema and steps below illustrate the pattern with a relational table. They are not a tested implementation. Adapt them to your engine and isolation level.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
CREATE TABLE IdempotencyRecords (
    TenantId        NVARCHAR(64)  NOT NULL,
    OperationName   NVARCHAR(100) NOT NULL,
    IdempotencyKey  NVARCHAR(255) NOT NULL,
    RequestHash     CHAR(64)      NOT NULL,   -- SHA-256 of the canonical request
    State           TINYINT       NOT NULL,   -- 1 = in progress, 2 = completed
    ResponseStatus  INT           NULL,
    ResponseBody    NVARCHAR(MAX) NULL,
    CreatedUtc      DATETIME2     NOT NULL,
    ExpiresUtc      DATETIME2     NOT NULL,
    CONSTRAINT PK_IdempotencyRecords PRIMARY KEY (TenantId, OperationName, IdempotencyKey)
);

-- Scheduled pruning
DELETE FROM IdempotencyRecords WHERE ExpiresUtc < SYSUTCDATETIME();
  1. Require the header on endpoints that need it. Reject empty values and values longer than your documented maximum.
  2. Compute the fingerprint from the route, the operation name, and the canonical body.
  3. Begin a transaction and insert the record with State = 1, the scope columns, the fingerprint, and ExpiresUtc.
  4. If the insert violates the primary key, roll back the new transaction and read the existing record. If the fingerprint differs, return a conflict. If State = 2, return the stored status and body. If State = 1, apply your in-progress behavior. Many relational engines block a second insert of the same key until the first transaction commits or rolls back, in which case this branch sees a completed row. Confirm this for your engine.
  5. If the insert succeeded, run the business write, update the record with ResponseStatus, ResponseBody, and State = 2, then commit. A rollback removes the claim along with the mutation, so no orphaned in-progress record remains. If you have decided to store failure outcomes, write that record in a separate transaction after the rollback.

Troubleshooting duplicate and retry problems

Symptom Likely cause What to check
Duplicate records after timeouts POST retried without a key, or a new key generated on each attempt Generate the key once per logical operation, outside the retry loop, and log it with the operation ID
Conflict response on a retry The same key was reused with different content Canonicalize the body before hashing: property order, whitespace, and defaults. Exclude fields that the client legitimately changes between attempts
Retries hang or repeatedly report in-progress The original attempt crashed after claiming the key, in a design where the claim sits outside the business transaction Look for records stuck in State = 1. In the transactional design, rollback removes the claim. Otherwise add a lease timeout
Duplicate effect after several days The key expired and was pruned Compare retention with the longest client retry window, and add a business uniqueness constraint as a backstop
Retry volume multiplies during an outage The SDK, an application loop, and a gateway retry independently Count attempts per logical operation. Three layers that each make four attempts can send up to 64 calls for one user action
Client keeps receiving a stored 500 A failure outcome was saved and is replayed for that key Decide whether failure outcomes should be stored. If they are, the client needs a new key to retry

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.