Polly .NET: Practical Resilience Guide for 2026
Matteo Migliore

Matteo Migliore is an entrepreneur and software architect with over 25 years of experience developing .NET-based solutions and evolving enterprise-grade application architectures.

He has led enterprise projects, trained hundreds of developers, and helped companies of all sizes simplify complexity by turning software into profit for their business.

Your team has opened a post-mortem for the third time this quarter, and the cause, once again, is an external service that stopped responding.

How many on-call hours, how many apologies to customers, how many sprints burned chasing the same problem under a different name?

The problem is not your team's skill.

The problem is "optimistic code": code written assuming that external services will always respond, with no plan for when they stop.

In 25 years I have watched the same incident repeat itself under different names, in different companies.

Take Marco, who leads the technical team of a mid-market e-commerce business.

One night the payment gateway stopped responding, and it took the product catalogue down with it, even though the catalogue had nothing to do with payments.

His CEO heard about it from a customer on "X" (formerly Twitter) before hearing it from the team.

Let me be clear about one thing: it is not your team's fault.

Distributed networks fail, sooner or later: that is statistics, not bad luck.

Even a .NET team that keeps getting hit by network incidents can build correct resilience pipelines in a few weeks, working on code already in production, without freezing releases.

That is why you will not find snippets of code to copy here.

They age quickly, and no team has the time to rewrite a guide every time a version changes.

What you will find instead is a way of thinking about Polly, the resilience library for .NET, that your team can apply regardless of the version it uses today or two years from now.

If your team builds microservices, applications that call external APIs, or AI agents that depend on a language model, this article helps you understand what it should already know how to configure, and why.

What Polly is and why every production .NET application needs it

Polly .NET makes calls to external services resilient

Polly is an open source library that gathers the most established resilience patterns in software engineering.

It was created in 2013 by Michael Wolfenden and is today one of the most downloaded .NET Foundation packages on NuGet, right behind the ones signed by Microsoft.

The question that matters is not "what is Polly", but "why does my team need it".

The answer is simple: any call to an external system can fail.

The way your team handles that failure determines the perceived quality of your system.

An application that stops working because the database responds slowly for 30 seconds is not an elegant system.

It is a fragile system, no matter how clean the code inside it is.

In these cases the enemy has no official technical name, but it has a practical one: optimistic code, the kind a team writes when no one has ever asked it what happens if the external service does not respond.

The failures Polly handles fall into three distinct categories, and confusing them is the first mistake that leads to poor choices:

  • Transient errors: network timeouts, refused connections, momentary server errors. They resolve on their own within seconds or a few attempts.
  • Overloaded services: traffic limits exceeded, high latencies, degraded responses. Here you need to reduce traffic to the service, not increase it with more attempts.
  • Extended failures: services down for minutes or hours. The system must degrade gracefully, without dragging down the features that do not depend on that service.

Polly does not stop things from failing.

It handles them when they fail, and that is what keeps a system standing when one of its dependencies stops responding.

In practice this translates into concrete scenarios.

An e-commerce site keeps showing product pages even when the payment gateway is unreachable for a few minutes.

A business system keeps logistics running while the ERP provider is under maintenance.

An AI assistant answers with a reasonable message instead of blowing up when the language model is under load.

IHttpClientFactory: direct integration with Polly v8

Anyone on your team who used Polly a few years ago knows the old approach of separate rules, stitched together with a clumsy mechanism.

It worked, but it had real limits: composition was hard to read, integration with the rest of the application was manual, and gathering monitoring data took extra work.

The current version rewrites everything, introducing the resilience pipeline as its central idea.

A pipeline is an ordered sequence of rules that wraps an operation: each one intercepts the call, applies its own logic, and passes control to the next.

To get started you only need a single NuGet package, the one designed to work alongside HttpClientFactory.

If you also need scenarios that do not go through the network, you add a second one, designed to build tailor-made pipelines.

The difference from the past is easier to tell than it is to read in code.

Before, you built each rule on its own, the retry on one side, the circuit breaker on the other.

Then you glued them together with a mechanism that reversed the logical order relative to how they had been written: the first rule declared ended up being the innermost one, not the outermost.

It confused anyone on the team reading that code for the first time.

Today you build a single sequence declared in order instead: you add the retry, then the circuit breaker, then the timeout, one after another.

The order in which they are written is the order in which they run, from the outside inward.

The leap between the two versions can be summed up like this:

AspectPrevious approachResilience pipeline (v8)
Composing the rulesEach rule built separately, then joined with a clumsy mechanismA single sequence declared in order: retry, then circuit breaker, then timeout
Execution orderReversed relative to how the rules were written: the first declared became the innermostLinear: the writing order matches the execution order, from the outside inward
Integration with the applicationManualDirect with ASP.NET Core

The real turning point, though, is the direct integration with the rest of the ASP.NET Core application, which we will look at shortly.

Retry: how to configure new attempts without making the problem worse

Retry is the simplest pattern, and for that very reason the most misconfigured.

The basic logic is obvious: if an operation fails, try again.

The problem lies in the details: how many times, at what interval, for which errors, and what to do when the final attempt fails too.

The first typical mistake is a fixed interval between attempts.

If a hundred clients fail at the same instant, after a brief server blackout, they all retry together and generate a traffic spike worse than the original problem.

The solution combines two ingredients: a wait that grows with each new attempt, and a small random variation, called jitter, that stops different clients from showing up all at once.

Several parameters are configured:

  • How many attempts to allow
  • How long to wait before the first attempt
  • A ceiling beyond which the wait must never rise
  • Which errors genuinely deserve a second attempt

A network timeout or a momentarily overloaded service deserves one.

An error caused by a badly formed request does not: retrying will not fix it, it only wastes time, and in production time means resources paid for nothing.

When you call a service with a traffic limit, many providers tell you precisely how long to wait before trying again.

Ignoring that indication is counterproductive: you burn through your margin needlessly and risk a temporary block.

The correct logic is simple to describe: if the service says how long to wait, you wait exactly that, plus a small safety margin.

If it does not say, you fall back on the growing wait described above.

A practical rule: at most three or four attempts for operations that respond directly to a user, where every second counts.

You can go up to ten for background jobs, where latency matters less than the final result.

Three attempts, a growing wait, a safety margin: these are starting rules, not absolute truths.

The right configuration depends on the behaviour of the services your team calls every day, not on a value taken from a post on the internet.

Jitter, backoff, per-attempt timeout: if these concepts were not familiar to you, that is not your fault.

They are details no online tutorial really teaches, yet they are what separates a team that writes code from one that designs reliable systems.

Our C# Course builds these foundations from scratch, with the same method your team can apply right away.

Circuit breaker: the three phases that stop cascading failures

The circuit breaker takes its name from the electrical device: when the current exceeds a threshold, the circuit opens and interrupts the flow.

In software, when a service crosses a failure threshold, the circuit breaker blocks calls to that service, protecting both the caller and the callee.

The problem it solves is subtle.

Without a circuit breaker, a retry responds to failures by increasing traffic to a service that is already struggling.

The service that is straining to respond gets buried under more attempts, and the situation worsens instead of stabilising.

The circuit breaker moves through three phases.

In the first, the normal one, calls pass through and the system simply watches the failure rate.

When that rate crosses the threshold, the second phase kicks in: calls are blocked immediately, without even attempting the real one, giving the struggling service room to breathe.

After a pause comes the third phase, the trial one: a limited number of calls is admitted to see whether the service is back in operation.

If it goes well, things return to normal.

If it goes badly, the pause starts over.

You define a failure threshold beyond which the circuit opens, for example half of the calls gone wrong.

You also define a minimum number of requests to observe before trusting that threshold, so it does not trip at the first hiccup on a rarely used service.

You add how long the circuit stays open before granting a new attempt.

When the circuit is open, the application code receives a specific signal instead of the expected response, and must handle it explicitly.

In most cases it does not let that signal bubble up to the user, but intercepts it and returns a reasonable alternative, for example the last piece of data cached just before the service stopped responding.

A system without a circuit breaker is not scaling: it is just waiting for the moment when a slow service will drag down the healthy ones too.

Now it is clear why optimistic code collapses under pressure: the intermediate phases are missing, the thresholds are missing, the discipline of designing for failure instead of the happy path is missing.

But it is just as important to be clear about what an article like this can and cannot give you.

Reading it gives your team a way of thinking.

It does not give you the hours spent getting it wrong in production before calibrating it against your company's real traffic.

This is not about how many years of .NET your team has.

Experience and method are not the same thing: you can have ten years of experience and still configure a fragile pipeline, if no one ever taught you to design for failure instead of the happy path.

Here is what an IT director at a mid-market e-commerce company reports, in concrete terms, after redesigning the resilience pipelines with their own team:

"Before, every network incident cost us half a day of emergency management and one angry customer.

Now the system degrades, it does not crash, and the team handles it on its own, without waiting for the senior who had written that piece of code to come back."

It is the kind of result that comes from work more focused than an article alone can offer: in the mentored programme we analyse together the resilience pipelines your team already has in production, not the textbook ones.

The benefit is direct: the team stops discovering a configuration problem during an incident, and starts discovering it during a calm code review.

The deeper benefit is organisational: the team stops depending on a single senior who "knows how it works", and builds a method anyone can apply.

The cost of getting there is a fraction of what a company loses in a single unhandled production incident.

Timeout: how long your system can afford to wait before giving up

The timeout limits waits and blocks on external calls

The timeout is the simplest defence mechanism, and the one most often overlooked.

The default value in .NET is 100 seconds: in production, a call that does not respond ties up a thread, and a connection, for almost two minutes.

With Polly you define two distinct levels: one for the single attempt, one for the whole operation, retries included.

The distinction is fundamental, and confusing it is a mistake I have seen repeated in almost every company I have worked with.

The overall timeout must be registered before the retry in the sequence, the per-attempt one after.

This way the first wraps the entire operation, including all the waits between one attempt and the next, while the second resets with every new attempt and does not accumulate with the previous ones.

To calibrate the right values, a practical formula: measure the service's usual response time during peak periods, and multiply it by two or three to get the per-attempt timeout.

For the overall timeout, add that value up for each planned attempt, add the waits between one attempt and the next, and leave a margin.

For language models the situation changes: a response to a complex prompt can take up to a minute.

Here the timeout must be generous enough to cover the real latency, without cutting a legitimate response in half.

100 seconds by default in .NET.

If you did not know that, how many other implicit settings are you unaware of?

.NET is full of implicit behaviours that nobody flags until they cause an incident in production.

In the C# Course your team learns to spot them before they become a problem, not after.

Bulkhead and rate limiting: protecting resources when one service slows down all the others

Imagine a slow service that receives two hundred requests at the same instant.

Without a limit, each of those requests keeps a resource busy, a process, a connection, a block of memory, until it gets a response.

The rest of the application, the part that has nothing to do with the slow service, still ends up queued behind a bottleneck no one designed on purpose.

The bulkhead, which takes its name from the watertight compartments of ships, isolates resources: it assigns a maximum ceiling of parallel requests to a single service, with a queue for those that arrive once that ceiling is already reached.

When the queue fills up too, the most common choice is to reject the incoming request, leaving the queue unchanged, and to record the event to understand how much pressure the system is under.

Polly also offers a strategy to limit the traffic generated outward, useful when it is your company that has to respect a limit imposed by a provider, not the other way around.

The difference is subtle but important:

AspectBulkheadTraffic limiting (rate limiting)
What it protectsInternal resources (connections, memory, processes)The relationship with the external provider
How it actsImposes a maximum ceiling of parallel requests to a service, with a queue for those that arrive laterKeeps the traffic generated outward within the limits imposed by the provider
When you need itWhen a slow service risks occupying all the application's resourcesWhen it is your company that has to respect a limit imposed by a third party

Fallback: the last response when everything else has stopped working

One customer sees "data as of 5 minutes ago".

Another sees an error page.

Behind the scenes the problem was identical: a service that was not responding.

The fallback decides which of the two customers your company puts in front of them.

It is the last line of defence: it steps in when retry, circuit breaker and timeout have failed to get a useful response.

It defines what to return in their place, a default value, the last copy saved in cache, a partial response, instead of letting the error rise up to whoever is waiting.

In production, the fallback often combines with a cache: when the primary service does not respond, data is served from the last saved copy.

If the cache is empty too, because of a cold start or a recent update, the system still responds with a reasonable value instead of propagating an error.

It is not the perfect solution.

It is the one that keeps the customer experience standing while the real problem gets solved elsewhere.

From fallback to the final timeout: the ordering mistake that undoes everything else

The most frequent trap in a pipeline with several rules is placing the circuit breaker before the retry.

With this wrong order, the circuit breaker sees only the final result of all the attempts, not each single attempt, and loses the ability to open quickly during a series of failures.

Another common trap: forgetting the overall timeout and keeping only the per-attempt one.

With three attempts, a ten-second timeout each and growing waits between one attempt and the next, the whole operation can exceed fifty seconds.

On an endpoint that responds to a waiting customer, that is almost always unacceptable.

The right order follows a precise logic instead, from the outside inward:

  1. Fallback: catches whatever no other level has handled
  2. Overall timeout: limits the maximum time of the entire operation
  3. Retry: tries again when it makes sense
  4. Circuit breaker: blocks calls when the service is struggling
  5. Per-attempt timeout: the innermost of them all
With this order, each rule protects the next one, without overlapping its responsibilities.

It is a sequence that, once understood, you never forget: the first rule registered is the outermost, the last to see the final result.

Polly with HttpClientFactory in ASP.NET Core: the entry point of almost every modern application

IHttpClientFactory integrates Polly into .NET HTTP calls

Are you wondering whether all this resilience applies only to HTTP calls?

Almost all modern .NET applications go through them, and that is why integration with HttpClientFactory is the recommended way to use it.

HttpClientFactory manages the lifecycle of HTTP clients, avoiding the connection exhaustion problems typical of manual creation, and resilience hooks on top without touching the rest of the code.

The dedicated package offers two paths.

The first is a ready-made configuration, with reasonable default values: perfect for getting started right away, customising only what really matters, such as the number of attempts or the length of the circuit breaker's pause.

The second gives full control over building the pipeline, useful when a service's needs are too specific for a standard configuration.

Not just HTTP calls: databases, queues, file systems

Polly is not only for network calls.

Any operation that can fail transiently is a candidate for resilience, and databases are the most common example: simultaneous locks, connection timeouts, brief server unavailability happen more often than you would think.

In these cases, the retry intercepts only the truly transient errors, recognisable by their specific code, and lets all the others propagate, the ones a new attempt would not solve anyway.

Polly metrics: the earliest signals that something is breaking

Has your team ever discovered a configuration problem only because a customer wrote in to complain, rather than from an automatic alert?

That is the clearest sign that the right metrics are missing.

A resilience pipeline without this data is like a circuit breaker with no indicator light to signal when it opens: you do not know when it happens, you do not know how often, and you only find out once it is already a problem visible to customers.

Polly integrates natively with the .NET metrics system and with OpenTelemetry.

You just hook the dedicated source into the configuration, together with the standard instrumentation for HTTP calls, and the data starts flowing automatically toward the chosen tool, whether that is Prometheus, the console or another monitoring system.

The information you get from it covers three aspects.

How many resilience events fired and of what kind: a retry, a circuit breaker state change, a timeout.

How long each execution takes, useful for understanding where the real latency hides.

And how many attempts it took to complete an operation.

The dashboards that really matter

With Prometheus and Grafana, this data becomes operational dashboards the team can check without opening the code.

The four most useful are these:

  • The retry rate per single service: a sudden increase signals an external problem in progress
  • The circuit breaker state: shows which services are struggling right now
  • The timeout percentage: tells you whether the values are calibrated too aggressively
  • The fallback rate: measures how often the system is serving degraded data instead of the real thing
A piece of advice matured across several projects: set an alert when a circuit breaker opens or when retries exceed 20% within a 5-minute window.

They are the earliest signals of a problem in external dependencies, well before a customer notices.

The right metrics are designed, not discovered after an incident.

Knowing what to measure, and why, comes from the same discipline behind every well-written line of code.

It is what we train in the C# Course: not just syntax, but the way of thinking that leads a team to build observable systems from the very first commit.

Polly for AI agents: when the cost of an error is not just lost time

If your team builds AI agents with Semantic Kernel or systems that retrieve information from a knowledge base, resilience takes on a different character.

Providers like OpenAI and Azure OpenAI impose aggressive limits over 60-second windows, with responses that can take anywhere from half a second to a full minute depending on the complexity of the request.

Model updates, in turn, cause brief periods of unavailability that you would almost never see with a traditional service.

When you exceed those limits, the provider responds with a precise error and, almost always, an indication of how many seconds to wait before trying again.

Ignoring it and using a generic wait is a mistake as common as it is costly: if you are asked to wait 20 seconds and you retry after 4, you waste the attempt, you get another rejection, and you burn through even more margin.

The difference compared to a traditional service shows up on three fronts:

AspectTraditional external serviceLLM provider (OpenAI, Azure OpenAI)
Traffic limitsGenerally stable over timeAggressive, calculated over 60-second windows
Response timePredictableFrom half a second to a full minute, depending on the complexity of the request
Cost of a wrong retryLatency lostLatency lost plus tokens paid for a response that will never arrive

The correct strategy respects that value, with a small extra safety margin, and falls back on a growing wait only when there is no explicit indication.

The circuit breaker, in this scenario, has to be tuned more cautiously than for an ordinary service.

With a model in difficulty, each extra attempt is not just lost latency.

It is a real cost in tokens paid for a response that will never arrive, a line of spending that someone in the company will have to justify at the end of the month.

For applications that cannot afford an interruption, an advanced pattern adds a fallback to an alternative provider.

If the primary service is at its limit, you temporarily switch to a second provider.

Dedicated timeout, retry and circuit breaker for the first still remain, so the switch happens only when it is truly needed.

Configuring Polly correctly makes the difference between an agent that degrades gracefully and one that hammers the provider, generating unexpected costs.

The same principle, applied on a larger scale, holds for any distributed architecture: applications spread across several microservices usually have different levels of external calls, and each one deserves its own pipeline.

A payment service deserves wider timings and a more cautious circuit breaker than a non-critical notification service.

Configuring Polly in production: what changes compared to development

In development you want to see the effect of an error right away: detailed logs, almost no waits, a single attempt before giving up.

In production you need the exact opposite: essential logs, thresholds calibrated on real traffic, metrics collected continuously.

The same pipeline can read these values from the rest of the application and adapt on its own to the environment it runs in, without needing two parallel versions to maintain.

It is worth keeping these values out of the code, in a configuration file or in a dedicated service such as Azure App Configuration.

When a problem emerges in production, being able to lengthen a circuit breaker's pause without a new release makes the difference between 5 and 30 minutes of downtime.

It is also the difference between a minor incident and one that ends up on the CEO's desk.

Verifying that resilience works, before a customer finds out

Testing the circuit breaker prevents production failures

"We configured the retry, it will work" is a phrase I hear often from teams with years of experience, and it almost always comes just before an incident.

Without a real check you do not know whether the circuit breaker actually opens, whether the attempts respect the growing wait you configured, or whether the fallback returns the right data when everything else fails.

The problem, historically, was time: verifying a pipeline with several attempts and growing waits could take minutes just to check the error cases.

For a few versions now, .NET has offered a way to simulate time, advancing it at will instead of really waiting for it.

You can make three attempts with their growing waits fire in a handful of milliseconds of real execution, verifying that the calculated timings are exactly the ones expected.

With the same principle you can simulate the responses of an external service, for example making the first attempts fail and only responding correctly from the next one.

This lets you verify that the circuit breaker opens after the configured threshold and closes again after the expected pause, without ever waiting a single real second on the clock.

It is worth adding these checks to the continuous integration pipeline.

They are not the fastest of all, but they stay quick enough not to slow it down, and knowing that the circuit breaker behaves as expected is one of the few solid guarantees a company can have about a distributed system.

Every external service called without a resilience pipeline is an incident just waiting for the wrong moment to happen.

It is not a question of if, it is a question of when: and when it happens in production, the bill is paid by the team that will be on call that night, and by the customer who notices before you do.

In the programme, a tutor works directly with every team we accept: we cannot do that for an unlimited number of companies in parallel, so places are limited by design, not by marketing.

The programme is by application: we assess your team's context before proposing anything, it is not meant for those looking for a shortcut, but for those who want to build a method that lasts even when the people on the team change.

The question is not whether the next timeout will catch your team by surprise.

It is whether, when it happens, your system will degrade gracefully or drag down everything else.

Marco has not seen a network error turn into a total outage in the months since.

Not because his team learned more technologies.

Because it learned to design for failure instead of hoping it would never come.

When you want the same result for your team, start here.

There is a single real difference between Marco's team today and Marco's team a year ago: it has not learned more .NET.

It has stopped designing on the assumption that everything always goes well.

Your team can do the same, but not by improvising for two hours on a Saturday with a tutorial.

The C# Course exists for this: to build, line by line, the discipline of those who design for failure instead of hoping it never comes.

The teams that stop designing only for the perfect scenario do it by choice, not because something has already forced them to.

You decide which side you want yours to be on.

Frequently asked questions

Polly is an open source .NET library that implements resilience patterns like retry, circuit breaker, timeout, bulkhead and fallback. It makes applications more robust against transient errors, unavailable external services and overload conditions, preventing a single failure from cascading through the system.

Polly v8 introduces the Resilience Pipeline as the central construct, replacing individual Policies. The new API is deeply integrated with Microsoft.Extensions.Http.Resilience and the modern .NET ecosystem. The old Policy.Handle and WrapAsync approach is still supported but deprecated. With v8 you define pipelines using ResiliencePipelineBuilder and configure them via AddResilienceHandler in ASP.NET Core.

With Polly v8 you use AddRetry() on the ResiliencePipelineBuilder. You can configure MaxRetryAttempts, Delay, BackoffType (constant, linear or exponential) and ShouldHandle to specify which exceptions or results trigger a retry. Automatic jitter prevents the thundering herd problem when many clients retry simultaneously.

The circuit breaker pattern automatically stops calls to a failing service, preventing you from overwhelming a system already under stress. It has three states: Closed (normal operation), Open (calls are blocked without even attempting) and Half-Open (a limited number of attempts are allowed to verify recovery). Use it whenever you call external services or databases that can become overloaded.

In ASP.NET Core you register the resilience pipeline using AddResilienceHandler() on IHttpClientBuilder. Every HTTP request made through that client automatically benefits from retry, circuit breaker and timeout, without having to manually manage the pipeline in application code.

Leave your details in the form below

Matteo Migliore

Matteo Migliore is an entrepreneur and software architect with over 25 years of experience developing .NET-based solutions and evolving enterprise-grade application architectures.

Throughout his career, he has worked with organizations such as Cotonella, Il Sole 24 Ore, FIAT and NATO, leading teams in developing scalable platforms and modernizing complex legacy ecosystems.

He has trained hundreds of developers and supported companies of all sizes in turning software into a competitive advantage, reducing technical debt and achieving measurable business results.

Stai leggendo perché vuoi smettere di rattoppare software fragile.Scopri il metodo per progettare sistemi che reggono nel tempo.