When we develop an application, we often assume, perhaps a little too optimistically, that everything will work. This is especially true when we communicate with other services or third-party systems.
We assume the external service will always be available, the network will work, responses will arrive quickly, and once communication is established, it will keep working the same way.
Consider a simple example:
Normally, Catalog API responds in 100 ms and everything works perfectly.
Then, for a few seconds, Catalog API returns 503 Service Unavailable. Or it takes eight seconds to respond, stops responding altogether, or gets overwhelmed when our application receives ten times its expected traffic.
A dependency can become unavailable, slow, or overloaded. We need to design our system knowing that sooner or later this will happen.
This is where resilience comes in.
From fault handling to resilience
The first instinct is to handle the error:
try
{
var response = await catalogClient.GetAsync("/products/42");
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
// Handle error
}
There is nothing wrong with this code, but it only decides what to do after the operation has failed.
Making an application resilient means going a step further. We need to ask:
- What happens if the call fails?
- What happens if the service is slow?
- What happens if it keeps failing?
- What happens if we send too many requests?
- What happens if the failure is only temporary?
Resilience means explicitly designing how an application behaves when failures occur: recovering where possible and, above all, preventing a problem in one dependency from spreading through the rest of the system.
This is where Microsoft.Extensions.Resilience helps.
Microsoft.Extensions.Resilience
If you have been building resilient .NET applications for a while, you probably know Polly, one of the most widely used .NET libraries for strategies such as retries, circuit breakers, and timeouts.
With .NET 8, Microsoft introduced two packages built on Polly v8: Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience, as described in its article on .NET 8 resilience.
Today we have:
Microsoft.Extensions.Resilience, which provides general mechanisms for building and integrating resilience pipelines.Microsoft.Extensions.Http.Resilience, which adds features specifically forHttpClient.
The older Microsoft.Extensions.Http.Polly package is deprecated. Microsoft recommends the newer packages for new applications; its .NET resilience documentation has the current guidance.
The conceptual model will feel familiar if you have used Polly, since the Microsoft packages are built on it. Before looking at code, let’s see what problems these strategies solve.
An overview of the strategies
A resilience pipeline is a sequence of strategies wrapped around the operation we want to execute.
For an HTTP call, we can picture it like this:
This is the standard pipeline provided by Microsoft.Extensions.Http.Resilience. In order, it contains a rate limiter, an overall timeout, a retry strategy, a circuit breaker, and a timeout for each attempt.
For now, a high-level understanding of each strategy is enough:
- The rate limiter controls how many operations can run concurrently against the dependency. Its default is 1,000 concurrent operations with no queue. The aim is to avoid putting excessive pressure on the called service.
- The total timeout limits the whole operation, including any subsequent attempts.
- The retry strategy repeats an operation when a failure might be transient.
- The circuit breaker temporarily stops calls when the failure rate for a dependency exceeds a threshold. By default, that threshold is 10% of requests within a 30-second window, with at least 100 requests.
- The attempt timeout limits the duration of each individual attempt.
Configuration can be surprisingly simple. After adding the package:
dotnet add package Microsoft.Extensions.Http.Resilience
one line adds the standard handler:
services
.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("https://catalog.example.com");
})
.AddStandardResilienceHandler();
AddStandardResilienceHandler() attaches the standard pipeline to our HttpClient. Adding that line alone, however, does not magically make every application resilient.
Retrying is not always a good idea
Suppose we receive 503 Service Unavailable. The problem may be temporary, so retrying can make sense.
If we receive 400 Bad Request, waiting a second and sending the same request again is unlikely to change the result. The standard pipeline does not retry it: retry and circuit-breaker strategies handle 5xx, 408 Request Timeout, 429 Too Many Requests, HttpRequestException, and timeouts.
The case of a non-idempotent POST /payments is more interesting. If we do not know whether the first attempt was processed, automatically repeating it might result in two payments.
The standard pipeline does not protect us here. By default, it retries requests made with any HTTP method, including POST. If our client performs non-idempotent operations, we must configure this explicitly:
services
.AddHttpClient("payments", client =>
{
client.BaseAddress = new Uri("https://payments.example.com");
})
.AddStandardResilienceHandler(options =>
{
// No retries for POST, PUT, PATCH, DELETE, or CONNECT
options.Retry.DisableForUnsafeHttpMethods();
});
Every resilience strategy introduces trade-offs. Even an apparently harmless retry adds traffic. If hundreds of instances retry at the same time against a struggling service, they can make its problems worse. The standard pipeline reduces this risk through exponential backoff with jitter, which spreads attempts over time, and a circuit breaker that stops calling a clearly struggling dependency. Reducing the risk does not eliminate it.
Resilience does not mean retrying everything. It means understanding the failure you are trying to handle and the behavior you want.
What about Polly?
Because Microsoft.Extensions.Resilience is built on Polly, there is one more point to consider.
In July 2026, Polly announced its adoption of the Open Source Maintenance Fee (OSMF). Starting November 16, 2026, the announced fee is US$20 per month per organization for those meeting the project’s criteria: at least US$20,000 in revenue from products that use Polly. The code remains open source and Polly’s license does not change.
There is an important distinction: according to the OSMF FAQ for software consumers, the fee concerns direct dependencies, not transitive ones. If we use Microsoft.Extensions.Resilience without referencing Polly directly, Polly remains a transitive dependency of the Microsoft package.
The distinction concerns referenced packages, not the code we write. Using Polly types exposed by the Microsoft packages, such as RetryStrategyOptions or the Polly namespace, does not change it. Adding an explicit PackageReference to Polly.Core or Polly.Extensions does make Polly a direct dependency.
Teams that reference Polly directly need to assess their own case. Teams that use it transitively through Microsoft.Extensions.Resilience are in a different position under the OSMF rules currently published.
With that aside, let’s return to resilience.
Resilience by design
We started with a simple HTTP call. At first, there seemed to be little to design: send a request, receive a response. Once we stop assuming everything will always work, however, we have to decide what happens if the call fails, slows down, or puts pressure on the external service.
A resilience pipeline lets us turn these questions into explicit application behavior.
The key idea is:
A resilient application is not one that never fails.
It is one designed with the knowledge that something will eventually fail.
In the next article we will step inside a resilience pipeline. We will deliberately make our call to Catalog API fail and examine timeouts, retries, circuit breakers, rate limiting, and hedging: how they behave, when to use them, and when they can make things worse.