Most .NET teams do not have to pay anything when Polly’s maintenance fee starts on 16 November 2026. The fee only applies to direct references to the next major Polly version, and Polly’s own README says that using it through Microsoft.Extensions.Http.Resilience is exempt. If you want zero Polly in your dependency graph, you can use Kevlar or about 150 lines of your own DelegatingHandler code.
A lot of developers and CTOs read “Polly is going paid” and started planning a migration. In this article, I will walk you through the fee terms, a one-command check for your projects, and three ways to get retries, timeouts and a circuit breaker without referencing Polly yourself. I ran all three against the same flaky API, and one has a default that can create duplicate orders. Let’s get into it.
The source code, including a fake API that fails on purpose and the test harness, is on GitHub. I tested on .NET runtime 10.0.14 on Windows 11 with Microsoft.Extensions.Http.Resilience 10.11.0 and Kevlar.Extensions.Http 1.4.1. The fee facts are as of 5 November 2026.
What Changes When the Polly Fee Starts on 16 November 2026?
Polly adopted the Open Source Maintenance Fee (OSMF) in July 2026. The source code stays open under the same BSD-3-Clause license, and nothing stops working on 16 November. What changes is that companies above a revenue threshold are asked to pay for using the maintained binary releases, such as the packages on NuGet.
| Question | Answer (as of 5 November 2026) |
|---|---|
| How much? | US $20 per month, per organization (one fee covers all your projects) |
| Who pays? | Companies earning at least US $20,000 from a product that uses Polly |
| From when? | Payment requests start on 16 November 2026, through GitHub Sponsors |
| Which versions? | Only the next major version released after 16 November 2026. Existing versions, including Polly 8.9.0, are not subject to the fee |
| Transitive use? | Exempt. The Polly README names Microsoft.Extensions.Http.Resilience as an example |
| License change? | No |
The version and transitive rules came in an update on 12 October 2026, so posts and videos from before that date describe a stricter situation. The update says: “Only direct references to Polly are subject to the fee.” The OSMF consumer FAQ says the same about transitive dependencies.
Two questions remain open, since Polly has not said whether using Polly types exposed by the Microsoft packages counts as direct use, and adding a direct Polly reference to patch a security issue makes you a direct user. Microsoft said in dotnet/extensions#7899 on 30 October that it is exploring whether to keep the Polly dependency, with no decision yet.
Before looking at alternatives, I want to be clear about one thing. Polly has handled retries for .NET developers for more than ten years, with over 2 billion downloads across its packages. If your company earns more than $20,000 from a product that references Polly directly, paying $20 a month is the right thing to do, and it costs less than a day of migration work.
Do You Have to Pay the Polly Maintenance Fee?
If you earn under $20,000 from the product, you owe nothing. For everyone else, this table follows the published terms, and unusual cases should go to your legal team.
| Your situation | Fee today? | What I would do |
|---|---|---|
| Direct reference to Polly 8.x | No | Pin 8.x so nobody upgrades to the next major by accident, or use option 1 |
Polly only arrives through Microsoft.Extensions.Http.Resilience | No | Nothing. This is the exempt path |
| Direct reference to the next major Polly version, company above $20K | Yes | Pay the fee, or use option 1 |
| Legal wants no Polly in the dependency graph | No fee involved | Kevlar (option 2) or hand-rolled handlers (option 3) |
How Do You Check Whether Your App References Polly Directly?
Run this from your solution folder. It lists your packages and the ones they pull in.
dotnet list package --include-transitiveIf Polly or Polly.Core appears under “Top-level Package”, you reference it directly. If it only appears under “Transitive Package”, another library brings it in. In my sample, Microsoft.Extensions.Http.Resilience 10.11.0 pulled in Polly.Core, Polly.Extensions and Polly.RateLimiting 8.4.2, the minimum version Microsoft declares, while the latest Polly is 8.9.0. Raising that version means adding a direct reference, which makes you a direct user. My NuGet package guardrails article covers keeping surprise dependencies out.
Option 1: Use Microsoft.Extensions.Http.Resilience
For most teams, the simplest move is to delete the direct Polly reference and use Microsoft’s HTTP resilience package, which adds a rate limiter, timeouts, retries and a circuit breaker to any HttpClient from IHttpClientFactory. Microsoft’s testfx repository made this exact switch in PR #11951 to avoid an accidental upgrade to a future Polly major version.
// dotnet add package Microsoft.Extensions.Http.Resilience --version 10.11.0using Microsoft.Extensions.Http.Resilience;
builder.Services.AddHttpClient("catalog", c => c.BaseAddress = catalogUrl) .AddStandardResilienceHandler(o => o.Retry.DisableForUnsafeHttpMethods());The DisableForUnsafeHttpMethods() call is there on purpose. In my test, the standard handler retried a POST by default, so a request that failed once with 503 reached the server twice and created the order on the second attempt. If that endpoint is not idempotent, a customer can be charged twice. My HttpClient and IHttpClientFactory article shows the same problem and explains the defaults.
Option 2: Use Kevlar, a Resilience Library With No Polly Inside
Kevlar is an MIT-licensed resilience library that does not depend on Polly at all. It supports retry, timeout, circuit breaker, hedging, fallback and rate limiting, and its HTTP package has a matching registration, so AddStandardResilienceHandler() becomes AddStandardShield(). Its migration guide warns that the defaults differ.
// dotnet add package Kevlar.Extensions.Http --version 1.4.1builder.Services.AddHttpClient("catalog", c => c.BaseAddress = catalogUrl) .AddStandardShield();Its documented defaults are a 30-second total timeout, a 10-second attempt timeout, three jittered retries that honour Retry-After, and a breaker that opens at a 50% failure ratio over 30 seconds. In my test it did not retry the POST.
My concern with Kevlar is maturity. The first version shipped on 20 August 2026 and it has one maintainer, although it already has 176,000 downloads. It is not API-compatible with Polly, so custom pipelines need rewriting. Paramore.Fences.Core, a fee-free fork of Polly 8.7.0, keeps the Polly API and is still pulling in upstream changes, but with around 12,900 downloads I would watch it before using it in production.
Option 3: Write the Resilience Handlers Yourself
If you want no third-party resilience package at all, three small DelegatingHandler classes cover what most APIs need. A DelegatingHandler sits in the HttpClient pipeline and can inspect or resend a request. The first one cancels a single attempt that hangs and turns it into a TimeoutException, so the retry handler treats it as transient.
public sealed class AttemptTimeoutHandler(TimeSpan timeout) : DelegatingHandler{ protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(timeout); try { return await base.SendAsync(request, cts.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException($"The attempt timed out after {timeout.TotalSeconds}s."); } }}The retry handler only retries idempotent methods. It retries on 408, 429, any 5xx, HttpRequestException and attempt timeouts, honours Retry-After, and otherwise waits with exponential backoff plus jitter so that many clients do not retry at the same moment.
public sealed class RetryHandler(int maxRetries, TimeSpan baseDelay, TimeSpan maxDelay) : DelegatingHandler{ private static readonly HashSet<HttpMethod> IdempotentMethods = [HttpMethod.Get, HttpMethod.Head, HttpMethod.Options, HttpMethod.Put, HttpMethod.Delete];
protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (!IdempotentMethods.Contains(request.Method)) return await base.SendAsync(request, cancellationToken);
for (var attempt = 0; ; attempt++) { HttpResponseMessage? response = null; try { response = await base.SendAsync(request, cancellationToken); if (!TransientHttp.IsTransient(response) || attempt == maxRetries) return response; } catch (Exception ex) when (TransientHttp.IsTransient(ex) && attempt < maxRetries && !cancellationToken.IsCancellationRequested) { }
var delay = GetDelay(response, attempt); response?.Dispose(); await Task.Delay(delay, cancellationToken); } }
private TimeSpan GetDelay(HttpResponseMessage? response, int attempt) { var retryAfter = response?.Headers.RetryAfter; if (retryAfter?.Delta is { } delta) return Min(delta, maxDelay); if (retryAfter?.Date is { } date) { var wait = date - DateTimeOffset.UtcNow; return wait <= TimeSpan.Zero ? TimeSpan.Zero : Min(wait, maxDelay); } var exponential = baseDelay * Math.Pow(2, attempt); return Min(exponential * (0.5 + Random.Shared.NextDouble()), maxDelay); }
private static TimeSpan Min(TimeSpan a, TimeSpan b) => a < b ? a : b;}TransientHttp is a small helper in the repo that matches 408, 429, 5xx, HttpRequestException and TimeoutException. Resending the same HttpRequestMessage works because string and JSON content are buffered, so buffer any stream body first.
The circuit breaker is where most hand-rolled versions go wrong. IHttpClientFactory rebuilds the handler chain every two minutes by default, so if you keep the failure count inside the handler, the breaker resets itself without telling you. I keep the state in a separate class that lives as long as the client registration.
public sealed class CircuitBreakerState(int failureThreshold, TimeSpan breakDuration, TimeProvider time){ private readonly Lock _lock = new(); private int _failures; private DateTimeOffset? _openedAt; private bool _probeInFlight;
public void EnsureCanSend() { lock (_lock) { if (_openedAt is null) return; if (time.GetUtcNow() - _openedAt.Value < breakDuration || _probeInFlight) throw new CircuitOpenException($"The circuit is open for {breakDuration.TotalSeconds}s."); _probeInFlight = true; // half-open: let one request test the service } }
public void RecordSuccess() { lock (_lock) { _failures = 0; _openedAt = null; _probeInFlight = false; } }
public void RecordFailure() { lock (_lock) { _probeInFlight = false; if (++_failures >= failureThreshold || _openedAt is not null) _openedAt = time.GetUtcNow(); } }
public void ReleaseProbe() { lock (_lock) _probeInFlight = false; }}The CircuitBreakerHandler in the repo calls EnsureCanSend() before each attempt and records the outcome. CircuitOpenException does not derive from HttpRequestException, so the retry handler stops at an open circuit. An extension method wires the handlers in the same order as the Microsoft standard handler: total timeout, retry, circuit breaker, attempt timeout.
public static IHttpClientBuilder AddHandRolledResilience( this IHttpClientBuilder builder, Action<HandRolledResilienceOptions>? configure = null){ var options = new HandRolledResilienceOptions(); configure?.Invoke(options); var breaker = new CircuitBreakerState(options.FailureThreshold, options.BreakDuration, TimeProvider.System);
return builder .ConfigureHttpClient(client => client.Timeout = options.TotalTimeout) .AddHttpMessageHandler(() => new RetryHandler(options.MaxRetries, options.BaseDelay, options.MaxDelay)) .AddHttpMessageHandler(() => new CircuitBreakerHandler(breaker)) .AddHttpMessageHandler(() => new AttemptTimeoutHandler(options.AttemptTimeout));}You then call .AddHandRolledResilience() on AddHttpClient() like the other two options. The breaker is created once when the client is registered with dependency injection and captured by the handler factory, so it survives IHttpClientFactory rebuilding the handlers. This version opens the circuit after consecutive failures, which is simpler than the libraries’ failure ratios, and has no hedging or telemetry. If you need those, pick one of the libraries I recommend for ASP.NET Core.
How Do the Three Options Compare When a Service Fails?
I gave all three the same settings: a 10-second total timeout, a 1-second attempt timeout, 3 retries from a 200 ms base delay, and a 5-second break. “Server hits” is how many requests reached the fake API.
| Scenario | Hand-rolled | Microsoft | Kevlar |
|---|---|---|---|
| 503 twice, then 200 | 200, 3 hits, 536 ms | 200, 3 hits, 509 ms | 200, 3 hits, 622 ms |
429 with Retry-After: 1, then 200 | 200, 2 hits, 1,011 ms | 200, 2 hits, 1,007 ms | 200, 2 hits, 1,013 ms |
| First call hangs 5 s, then fast | 200, 2 hits, 1,214 ms | 200, 2 hits, 1,220 ms | 200, 2 hits, 1,167 ms |
POST fails once with 503 | 503, 1 hit | 201, 2 hits | 503, 1 hit |
| Service down, 20 calls in a row | 5 hits, then 19 open-circuit errors | 5 hits, then 19 open-circuit errors | 5 hits, then 19 open-circuit errors |
I ran the harness four times, and the status codes and server hits were identical in every run, while timings moved a few hundred milliseconds because of jitter. All three honoured Retry-After, cut off the hanging attempt at one second, and stopped calling the dead service after 5 requests. The only real difference was the POST retry, which you fix with one line.
In my first run, sharing one client across scenarios let earlier failures open the Microsoft and Kevlar circuits early, so give each downstream dependency its own named client.
Which Option Should You Pick?
My take is that if you use Polly directly today, you should move to Microsoft.Extensions.Http.Resilience with DisableForUnsafeHttpMethods(). It is the exempt path named in Polly’s own README, and Microsoft maintains it. If your company is above the threshold and you want the next major Polly version for its full API, pay the fee and support the people who maintain it.
I would only reach for Kevlar or hand-rolled handlers when your legal or security team requires zero Polly in the dependency graph. For a small API with a few downstream calls, I would pick the hand-rolled handlers because the code is short and you own all of it. For a larger system that needs hedging and telemetry, Kevlar saves you from rebuilding those features, if you are comfortable with how young it is.
Key Takeaways
- Polly’s US $20 monthly fee covers only direct references to the next major version, in companies earning at least $20,000.
- Polly 8.x and transitive use through
Microsoft.Extensions.Http.Resilienceare exempt. - The Microsoft standard handler retries
POSTby default, so callRetry.DisableForUnsafeHttpMethods()unless your endpoints are idempotent. - Kevlar and about 150 lines of hand-rolled handlers behaved the same as the Microsoft handler in my tests, with no Polly at all.
Frequently Asked Questions
Is Polly still free to use?
Yes for most teams. The source stays open under BSD-3-Clause, existing versions like Polly 8.9.0 are not subject to the fee, and companies earning under US $20,000 owe nothing. The US $20 per month fee covers only direct references to the next major version.
Does the Polly maintenance fee apply to Polly 8?
No. Polly's 12 October 2026 update says the fee applies only to the next major version released after 16 November 2026, and existing versions are not subject to it.
Do I have to pay if I only use Microsoft.Extensions.Http.Resilience?
No. Polly's README says that when Polly is a dependency of another library, such as Microsoft.Extensions.Http.Resilience, the fee does not apply. Polly has not yet said whether calling Polly types exposed by that package counts as direct use.
What is the best alternative to Polly in .NET 10?
For most teams it is Microsoft.Extensions.Http.Resilience, which is free and exempt as a transitive dependency. If you need zero Polly, use Kevlar (MIT) or your own DelegatingHandler classes.
Can I write a retry policy without Polly?
Yes. A DelegatingHandler can retry 408, 429, 5xx and HttpRequestException with Retry-After support and jittered backoff. Retry only idempotent methods, and keep circuit breaker state outside the handler because IHttpClientFactory rebuilds handlers every two minutes.
Will Microsoft remove Polly from its resilience packages?
Microsoft has not decided. On 30 October 2026 the .NET team said in dotnet/extensions issue 7899 that it is exploring whether to keep the Polly dependency or take a different approach, with no timeline.
Summary
Polly’s maintenance fee affects far fewer teams than the headlines suggested. If you reference the next major version directly and earn more than $20,000, pay the $20 per month and support a project the whole .NET ecosystem depends on. Everyone else can stay put, or move to any of the three options, which behaved the same in my tests once POST retries were off. If you are hardening an API, my guides on global exception handling, rate limiting and health checks are good next reads.
If you found this helpful, share it with your colleagues, and let me know in the comments which option you are going with.
Happy Coding :)
What's your take?
Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.