Resilience in .NET with Microsoft.Extensions.Resilience

Retries are the most visible resilience strategy and the easiest one to misuse. Repeating a failing call can recover from a short network problem. It can also multiply load, repeat a write and keep working long after the caller has left.

A resilience pipeline should be treated as an execution budget with four dimensions:

  • Time: How long may the complete operation and one attempt take?
  • Attempts: Which failures justify another call and how many calls are allowed?
  • Concurrency: How much pressure may one application instance create?
  • Correctness: Can a repeated, hedged or degraded result remain truthful?

Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience integrate Polly v8 pipelines with dependency injection, options, IHttpClientFactory and telemetry. The libraries are not new in .NET 11. As of August 2026, the stable 10.9 packages support .NET 8 and later, including net11.0.

Release status: The .NET 11 feature snapshot below reflects Preview 7 from August 2026. The examples pin stable resilience packages at 10.9.0; that is a reproducible baseline, not a claim that these are the latest serviced packages.

Pick the package by boundary

PackageTypical use
Microsoft.Extensions.Http.ResilienceHTTP-aware pipelines through IHttpClientFactory
Microsoft.Extensions.ResilienceGeneral pipelines for storage, queues, databases and delegates
Polly.Core / Polly.ExtensionsThe underlying strategy and registry APIs
Microsoft.Extensions.Http.PollyLegacy integration to remove during migration

The packages have their own servicing cadence. Targeting net11.0 does not imply an 11.x resilience package.

1dotnet add package Microsoft.Extensions.Resilience --version 10.9.0
2dotnet add package Microsoft.Extensions.Http.Resilience --version 10.9.0

Start with one total budget

The caller’s deadline controls the pipeline. For an API with a five-second latency objective, a possible budget looks like this:

1inbound request budget                         5.0 s
2application work outside the dependency       0.8 s
3serialization and network margin              0.7 s
4available dependency budget                   3.5 s
5
6attempt 1                                     1.2 s
7retry delay                                   0.2 s
8attempt 2                                     1.2 s
9remaining margin                              0.9 s

A default 30-second total timeout cannot fit into that request. The pipeline must stop before the upstream caller abandons the result.

The standard HTTP handler already composes concurrency limiting, total timeout, retry, circuit breaking and per-attempt timeout in a sensible order:

1concurrency limiter
2  -> total timeout
3     -> retry
4        -> circuit breaker
5           -> attempt timeout
6              -> HTTP request

Order is behavior. The total timeout covers the inner attempts and retry delays. It does not include waiting in the outer concurrency limiter’s queue. The attempt timeout applies to one execution of the inner HTTP handler. MaxRetryAttempts = 2 means one original call plus two retries, not two calls in total.

These handler timeouts end when the inner handler returns response headers; they do not bound later response-body buffering or JSON deserialization. The caller’s cancellation deadline must also cover reading and processing the body. HttpClient.Timeout additionally covers buffering with the default completion option, but only through headers with ResponseHeadersRead, as described in the HttpCompletionOption reference .

A standard HTTP baseline

 1using Microsoft.Extensions.Http.Resilience;
 2
 3WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
 4
 5IHttpClientBuilder inventoryClientBuilder =
 6    builder.Services.AddHttpClient<IInventoryClient, InventoryClient>(httpClient =>
 7    {
 8        httpClient.BaseAddress = new Uri("https://inventory.internal.example/");
 9    });
10
11inventoryClientBuilder.AddStandardResilienceHandler(options =>
12{
13    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(12);
14    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(3);
15
16    options.Retry.MaxRetryAttempts = 2;
17    options.Retry.Delay = TimeSpan.FromMilliseconds(300);
18    options.Retry.UseJitter = true;
19    options.Retry.DisableForUnsafeHttpMethods();
20
21    options.CircuitBreaker.FailureRatio = 0.20;
22    options.CircuitBreaker.MinimumThroughput = 20;
23    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
24    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
25});

The numbers are examples, not universal defaults. Dependency latency, caller deadlines, request volume and recovery behavior determine them.

The typed client remains responsible for the downstream HTTP contract:

 1using System.Net;
 2using System.Net.Http.Json;
 3
 4public sealed record InventoryAvailability(
 5    string ProductId,
 6    bool IsAvailable,
 7    int AvailableQuantity);
 8
 9public interface IInventoryClient
10{
11    Task<InventoryAvailability?> GetAvailabilityAsync(
12        string productId,
13        CancellationToken cancellationToken);
14}
15
16public sealed class InventoryClient(HttpClient httpClient) : IInventoryClient
17{
18    public async Task<InventoryAvailability?> GetAvailabilityAsync(
19        string productId,
20        CancellationToken cancellationToken)
21    {
22        using HttpResponseMessage response = await httpClient.GetAsync(
23            $"/inventory/{Uri.EscapeDataString(productId)}",
24            cancellationToken);
25
26        if (response.StatusCode == HttpStatusCode.NotFound)
27        {
28            return null;
29        }
30
31        response.EnsureSuccessStatusCode();
32
33        return await response.Content.ReadFromJsonAsync<InventoryAvailability>(
34            cancellationToken);
35    }
36}

Application code depends on IInventoryClient, not on Polly. That keeps transport recovery out of the domain layer and makes unit tests independent of a live network.

Retry only safe transient failures

Two conditions must be true before a retry: another attempt can plausibly succeed and repeating the operation is safe.

Connection resets, 408, selected 5xx responses, an acceptable 429 and an attempt TimeoutRejectedException may be transient. Validation errors, authorization failures, deterministic 404 responses, malformed payloads and programming defects are not.

The standard handler retries every HTTP method unless configured otherwise. A safe baseline calls DisableForUnsafeHttpMethods first and opts individual writes back in only when the server contract provides durable idempotency.

 1POST /payments
 2Idempotency-Key: abc-123
 3       |
 4       v
 5server stores key and result atomically
 6       |
 7connection fails after commit
 8       |
 9retry uses the same key
10       |
11server returns the stored result

Creating a fresh key inside the retry delegate defeats deduplication. A timed-out write without an idempotency mechanism has unknown state; retrying it can create a second effect.

Jitter avoids synchronized clients and a valid Retry-After should be honored when it fits inside the remaining total deadline. Waiting beyond that deadline is load amplification, not recovery.

Circuit breakers need a real failure domain

A circuit breaker stops calls after a failure ratio is reached within a sampling window. It then allows a probe after the break duration. It does not repair the dependency; it converts repeated slow failure into a fast local failure.

FailureRatio is ignored until MinimumThroughput is reached. A breaker requiring 100 samples may never open for a low-volume dependency, even if every call fails.

Circuit state is also local to one pipeline in one process. Ten replicas can observe ten different states. This must not be described as a global health signal.

The partition must match the dependency’s failure domain. One breaker shared by unrelated hosts creates correlated failure. One breaker per URL path fragments observations. For dynamic destinations, the authority is often a useful boundary:

 1using Microsoft.Extensions.Http.Resilience;
 2using Polly;
 3
 4IHttpClientBuilder clientBuilder =
 5    builder.Services.AddHttpClient("external-services");
 6
 7clientBuilder
 8    .AddResilienceHandler("per-authority", pipelineBuilder =>
 9    {
10        pipelineBuilder.AddCircuitBreaker(
11            new HttpCircuitBreakerStrategyOptions());
12    })
13    .SelectPipelineByAuthority();

Cancellation and timeout mean different things

A canceled caller no longer needs the result. A Polly timeout means the configured execution budget expired.

1caller cancellation -> stop attempts and delays -> propagate cancellation
2attempt timeout      -> one call was too slow   -> retry only if budget remains
3total timeout        -> pipeline budget ended   -> stop all inner work

Polly reports policy timeouts as TimeoutRejectedException, not TimeoutException. Retry predicates need to make that distinction and every downstream operation must observe the supplied CancellationToken.

The timeout hierarchy should also be documented. HttpClient.Timeout, total pipeline timeout, attempt timeout, proxy timeout and inbound deadline can all compete. One deliberate hierarchy is easier to diagnose than several defaults where the shortest value wins unpredictably.

General pipelines stay close to one dependency call

Non-HTTP operations use a named pipeline:

 1using Polly;
 2using Polly.Retry;
 3using Polly.Timeout;
 4
 5builder.Services.AddResiliencePipeline<string>(
 6    "catalog-read",
 7    static pipelineBuilder =>
 8    {
 9        pipelineBuilder.AddTimeout(TimeSpan.FromSeconds(6));
10
11        pipelineBuilder.AddRetry(new RetryStrategyOptions
12        {
13            ShouldHandle = new PredicateBuilder()
14                .Handle<TimeoutRejectedException>()
15                .Handle<CatalogUnavailableException>(),
16            MaxRetryAttempts = 2,
17            Delay = TimeSpan.FromMilliseconds(250),
18            BackoffType = DelayBackoffType.Exponential,
19            UseJitter = true
20        });
21
22        pipelineBuilder.AddTimeout(TimeSpan.FromSeconds(2));
23    });

The outer timeout bounds the complete execution; the inner timeout bounds one attempt.

 1using Polly;
 2using Polly.Registry;
 3
 4public sealed class CatalogService
 5{
 6    private readonly ICatalogStore _catalogStore;
 7    private readonly ResiliencePipeline _catalogReadPipeline;
 8
 9    public CatalogService(
10        ICatalogStore catalogStore,
11        ResiliencePipelineProvider<string> pipelineProvider)
12    {
13        _catalogStore = catalogStore;
14        _catalogReadPipeline = pipelineProvider.GetPipeline("catalog-read");
15    }
16
17    public ValueTask<CatalogItem?> GetAsync(
18        string productId,
19        CancellationToken cancellationToken)
20    {
21        return _catalogReadPipeline.ExecuteAsync(
22            async token => await _catalogStore.GetAsync(productId, token),
23            cancellationToken);
24    }
25}

Only the transient dependency operation belongs inside the pipeline. Retrying a complete use case that reads, writes, publishes a message and calls another service can repeat the parts that already succeeded.

Hedging spends capacity for tail latency

Retry waits for failure. Hedging starts another attempt while the first is still running. It can reduce tail latency when independent endpoints can answer the same safe request.

It can also double dependency traffic during a latency incident. Hedging two routes backed by the same saturated database usually makes the outage worse. It should be reserved for idempotent, cancelable reads with genuinely independent capacity and a measured tail-latency problem.

Overlapping writes require stronger idempotency than sequential retries. In most systems, writes should not be hedged at all.

Outbound limits complement inbound limits

ASP.NET Core rate limiting protects the application from inbound demand. A pipeline concurrency limiter protects a dependency from outbound demand. For the inbound side, see Production Rate Limiting in ASP.NET Core.

1clients -> inbound admission -> application -> outbound limit -> dependency

An API may accept 1,000 concurrent requests but allow only 50 calls to a fragile service. The rest need a bounded queue, a truthful degraded path or immediate failure.

Queues should remain at zero unless a measured latency budget justifies waiting. An unbounded queue converts saturation into memory growth. Limits are per process, so 50 permits across 20 replicas can still produce 1,000 downstream calls.

Fallbacks must tell the truth

A useful fallback returns a timestamped stale snapshot, omits an optional section or switches a read to an independent source. A dangerous fallback returns an empty list when data could not be loaded or reports a timed-out write as successful.

Degraded success is a separate product state. Responses should expose freshness or partial-result metadata where callers need it and fallback outcomes should be recorded separately from normal success.

Fallback order matters. An outer fallback sees the final failure after retry and breaker behavior. An inner fallback can turn every attempt into apparent success, preventing the outer strategies from seeing a failure at all.

Configuration reload changes live traffic policy

Pipelines can rebuild when named options reload. That is useful for adjusting retry and breaker settings without restarting a service, but the new pipeline may start with fresh circuit state.

Timeout relationships, ratios, delays and attempt counts need validation before activation. Every production change should be audited and rolled out like code because it changes request amplification and failure behavior immediately.

Telemetry must expose amplification

Polly emits pipeline duration, strategy events and attempt duration. The Microsoft integration adds bounded request and exception metadata:

1builder.Services.AddResilienceEnricher();

The important measurements are not only final failures:

  • attempts per logical operation;
  • recovered and exhausted retries;
  • total retry delay;
  • circuit transitions and rejected calls;
  • limiter rejections;
  • hedged attempts per result;
  • timeout source; and
  • degraded fallback rate.

A public endpoint can look healthy while retries consume most of the downstream capacity. Those metrics show the hidden cost.

Pipeline, strategy and operation names come from bounded code-defined values. Product IDs, complete URLs, user IDs, exception messages and idempotency keys do not belong in metric dimensions.

Tests should not sleep through production delays

HTTP policy tests can use a controlled HttpMessageHandler that returns a response sequence and records requests. Focused tests verify that a 503 followed by 200 retries once, a 400 does not retry, unsafe methods stay single-shot, cancellation stops attempts and retryable writes keep the same idempotency key.

Polly.Testing can inspect strategy order and configured options through GetPipelineDescriptor. Application-service unit tests can provide a test ResiliencePipelineProvider<string> that returns ResiliencePipeline.Empty for the requested name; the empty pipeline is not itself a provider. Separate policy tests execute controlled delegates with test-specific zero or short delays.

Integration tests still cover IHttpClientFactory, handler order, serialization, cancellation and telemetry. Above that, a proxy such as Toxiproxy injects latency and resets. The goal is not proving that a retry happened. It is proving that latency, dependency load and correctness stayed inside their budgets while it happened.

What .NET 11 adds around the pipeline

.NET 11 does not replace this programming model. It improves the transport around it:

  • GZipCompressedContent, BrotliCompressedContent and ZstandardCompressedContent can stream compressed request bodies. Retrying still repeats compression and transmission, so content must be safely recreated.
  • Experimental SocketsHttpHandler.ShouldEvictConnection can retire a suspect pooled connection. Evicting on every error would replace one issue with DNS, TCP and TLS churn.
  • FixedWindowRateLimiter now reports RetryAfter aligned with the next window boundary.
  • Happy Eyeballs, HTTP/3 latency work and network hardening can avoid failures before a resilience strategy reacts.

The best retry is often the one made unnecessary by a healthier transport path.

Review checklist

Before shipping a pipeline, the following questions require concrete answers:

  1. Which results and exceptions are transient?
  2. Is every repeated or hedged operation idempotent?
  3. What is the caller’s total deadline?
  4. How many dependency calls can one request create?
  5. What failure domain does the breaker represent?
  6. Can MinimumThroughput be reached at actual volume?
  7. How do per-process limits multiply across replicas?
  8. What does a fallback communicate?
  9. Does cancellation reach every operation?
  10. Which telemetry proves recovery without hiding amplification?

Without those answers, the pipeline is only configuration.

Design rule

A resilience strategy must reduce the impact of failure without creating a larger failure elsewhere. Retry spends attempts, hedging spends parallel capacity, circuit breaking spends availability for recovery time and fallback spends freshness or functionality.

AddStandardResilienceHandler is a strong baseline because it composes the common strategies coherently. The production work starts after registration: fitting that pipeline into one caller deadline, one dependency contract and one measurable load budget.

Further details are available in the .NET resilience documentation , the HTTP resilience guide and the Polly strategy documentation .

What Matters in ASP.NET Core 11

Sep 14, 2026 - 10 min read

What Matters in ASP.NET Core 11

ASP.NET Core 11 has no single feature that fundamentally changes how web applications are built. That is not a criticism. Mature frameworks …


Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms

Modernization

Training & Consulting