<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Polly on Russell ‛Russ’ Frith</title><link>https://russfrith.com/tags/polly/</link><description>Recent content in Polly on Russell ‛Russ’ Frith</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Wed, 09 Sep 2026 17:17:44 -0400</lastBuildDate><atom:link href="https://russfrith.com/tags/polly/index.xml" rel="self" type="application/rss+xml"/><item><title>Cracking Polly</title><link>https://russfrith.com/posts/cracking-polly/</link><pubDate>Wed, 09 Sep 2026 17:17:44 -0400</pubDate><guid>https://russfrith.com/posts/cracking-polly/</guid><description>Polly v8 replaced policies with resilience pipelines. A practical look at retry, circuit breaker, hedging, and chaos strategies in .NET for 2026 — plus what&amp;#39;s changed in the license.</description><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Polly is a .NET resilience and transient-fault-handling library. It lets you express strategies like retry, circuit breaker, timeout, rate limiting, hedging, and fallback around any piece of code, so a flaky downstream dependency doesn&rsquo;t automatically become an outage in your own service.</p>
<p>If you used Polly a few years ago and haven&rsquo;t looked at it since, the library you remember is gone. Polly v8 was a ground-up rewrite, built in collaboration with Microsoft, and it changes both the API and the mental model. This post is a practical, current tour of what Polly looks like today: pipelines instead of policies, the built-in HTTP resilience handlers, hedging, chaos engineering support, and the one thing that&rsquo;s changed about the license terms that maintainers of paid products should know about.</p>
<h2 id="policies-are-gone--pipelines-are-the-new-unit-of-composition">Policies are gone — pipelines are the new unit of composition</h2>
<p>In the old Polly (v7 and earlier), you built individual <code>Policy</code> objects and combined them with <code>PolicyWrap</code>. In v8, that&rsquo;s replaced entirely by the <strong>resilience pipeline</strong>: a single, ordered composition of strategies built with <code>ResiliencePipelineBuilder</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> pipeline = <span style="color:#66d9ef">new</span> ResiliencePipelineBuilder()
</span></span><span style="display:flex;"><span>    .AddRetry(<span style="color:#66d9ef">new</span> RetryStrategyOptions
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        MaxRetryAttempts = <span style="color:#ae81ff">3</span>,
</span></span><span style="display:flex;"><span>        Delay = TimeSpan.FromMilliseconds(<span style="color:#ae81ff">500</span>),
</span></span><span style="display:flex;"><span>        BackoffType = DelayBackoffType.Exponential
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .AddCircuitBreaker(<span style="color:#66d9ef">new</span> CircuitBreakerStrategyOptions())
</span></span><span style="display:flex;"><span>    .AddTimeout(TimeSpan.FromSeconds(<span style="color:#ae81ff">10</span>))
</span></span><span style="display:flex;"><span>    .Build();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> pipeline.ExecuteAsync(<span style="color:#66d9ef">async</span> token =&gt;
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// your call to a downstream dependency</span>
</span></span><span style="display:flex;"><span>}, cancellationToken);
</span></span></code></pre></div><p>Ordering is explicit and semantic: the first strategy added is the outermost one, and moving a timeout across a retry changes what your pipeline actually promises. A pipeline is also meant to be built once and reused: pipelines are thread-safe and designed to be cached, and building a new one per request just throws away the allocation-free execution path v8 was designed around.</p>
<p>The old policy names map fairly directly onto the new strategy builders, which makes migration mostly mechanical: <code>WaitAndRetryAsync</code> becomes <code>AddRetry</code>, <code>AdvancedCircuitBreakerAsync</code> becomes <code>AddCircuitBreaker</code> with failure-ratio options, <code>TimeoutAsync</code> becomes <code>AddTimeout</code>, <code>BulkheadAsync</code> becomes <code>AddConcurrencyLimiter</code>, <code>FallbackAsync</code> becomes <code>AddFallback</code>, and <code>PolicyWrap</code> disappears entirely because the pipeline itself is the composition. The legacy API still ships in the v8 package, so you don&rsquo;t have to migrate everything in one sitting.</p>
<h2 id="dependency-injection-and-named-pipelines">Dependency injection and named pipelines</h2>
<p>For anything beyond a toy example, you&rsquo;ll want to register pipelines at startup rather than build them inline. Polly integrates with <code>IServiceCollection</code> for exactly this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>services.AddResiliencePipeline(<span style="color:#e6db74">&#34;my-pipeline&#34;</span>, builder =&gt;
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    builder
</span></span><span style="display:flex;"><span>        .AddRetry(<span style="color:#66d9ef">new</span> RetryStrategyOptions())
</span></span><span style="display:flex;"><span>        .AddTimeout(TimeSpan.FromSeconds(<span style="color:#ae81ff">10</span>));
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> pipelineProvider = serviceProvider
</span></span><span style="display:flex;"><span>    .GetRequiredService&lt;ResiliencePipelineProvider&lt;<span style="color:#66d9ef">string</span>&gt;&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ResiliencePipeline pipeline = pipelineProvider.GetPipeline(<span style="color:#e6db74">&#34;my-pipeline&#34;</span>);
</span></span></code></pre></div><p>This gives you a <code>ResiliencePipelineProvider</code> that creates and caches pipelines by name, which is a much more testable pattern than reaching for a static policy instance scattered across your codebase.</p>
<h2 id="httpclient-gets-resilience-for-free">HttpClient gets resilience for free</h2>
<p>If most of your Polly usage has always been &ldquo;wrap an <code>HttpClient</code> call so it retries,&rdquo; you don&rsquo;t need to hand-roll a pipeline for that anymore. <code>Microsoft.Extensions.Http.Resilience</code>, built on top of Polly v8, ships ready-made resilience handlers for <code>HttpClient</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>services.AddHttpClient(<span style="color:#e6db74">&#34;orders-api&#34;</span>)
</span></span><span style="display:flex;"><span>    .AddStandardResilienceHandler();
</span></span></code></pre></div><p>That single line installs the recommended composition for service-to-service HTTP: a rate limiter, a 30-second total timeout, retry with three attempts using exponential jittered backoff that honors <code>Retry-After</code>, a circuit breaker, and a 10-second per-attempt timeout in exactly that outermost-to-innermost order. When the defaults don&rsquo;t fit your service, you can override the standard handler&rsquo;s options or drop down to <code>AddResilienceHandler</code> to compose something custom.</p>
<p>There&rsquo;s also a hedging variant for latency-sensitive calls. Hedging, introduced in Polly v8, improves request latency by issuing multiple concurrent requests instead of waiting for one to fail before retrying. The standard hedging handler swaps the retry strategy out for a hedging strategy: by default it fires a second request if no response comes back within two seconds, then takes whichever request finishes first, and it uses a pool of circuit breakers, typically keyed by URL authority, so requests don&rsquo;t keep getting hedged against an endpoint that&rsquo;s already unhealthy.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>services.AddHttpClient(<span style="color:#e6db74">&#34;search-api&#34;</span>)
</span></span><span style="display:flex;"><span>    .AddStandardHedgingHandler();
</span></span></code></pre></div><p>Hedging is a genuinely different tool from retry, it trades extra load for lower tail latency, so reach for it on read paths where a duplicate request is cheap and safe, not on anything with side effects.</p>
<h2 id="generic-pipelines-for-result-based-conditions">Generic pipelines for result-based conditions</h2>
<p>Not every failure is an exception. Sometimes &ldquo;failure&rdquo; means an <code>HttpResponseMessage</code> with a 503, or a domain object indicating a downstream system is unavailable. For that, Polly has generic pipelines:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> pipeline = <span style="color:#66d9ef">new</span> ResiliencePipelineBuilder&lt;OrderStatus&gt;()
</span></span><span style="display:flex;"><span>    .AddFallback(<span style="color:#66d9ef">new</span> FallbackStrategyOptions&lt;OrderStatus&gt;
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ShouldHandle = <span style="color:#66d9ef">new</span> PredicateBuilder&lt;OrderStatus&gt;()
</span></span><span style="display:flex;"><span>            .Handle&lt;HttpRequestException&gt;()
</span></span><span style="display:flex;"><span>            .Handle&lt;TimeoutRejectedException&gt;(),
</span></span><span style="display:flex;"><span>        FallbackAction = _ =&gt; Outcome.FromResultAsValueTask(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">new</span> OrderStatus { Status = <span style="color:#e6db74">&#34;Unknown&#34;</span> })
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .AddRetry(<span style="color:#66d9ef">new</span> RetryStrategyOptions&lt;OrderStatus&gt;
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        MaxRetryAttempts = <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .Build();
</span></span></code></pre></div><p><code>PredicateBuilder&lt;T&gt;</code> is the mechanism for saying &ldquo;treat this exception, or this particular result shape, as something worth retrying or falling back on&rdquo;. It&rsquo;s the same idea as the old <code>Handle&lt;T&gt;().OrResult(...)</code> chains, just consolidated into one type.</p>
<h2 id="chaos-engineering-built-in">Chaos engineering, built in</h2>
<p>One of the more interesting additions in the Polly v8 line is first-class support for chaos engineering strategies, deliberately injecting faults, latency, or outcome substitution into a pipeline so you can find out whether your resilience configuration actually works before production finds out for you. Rather than writing throwaway code to simulate a slow dependency, you can add a chaos strategy directly into a pipeline (typically gated to non-production environments or a small traffic percentage) and watch your retries, circuit breakers, and fallbacks respond to real, controlled failure. It&rsquo;s a natural complement to the rest of the pipeline model: the same composition mechanism you use for defense now doubles as your test harness for that defense.</p>
<h2 id="a-licensing-note-for-2026">A licensing note for 2026</h2>
<p>One change worth flagging if you maintain a commercial product on top of Polly: the project has adopted the Open Source Maintenance Fee model. Starting November 16, 2026, companies earning at least $20,000 USD from a product or project that uses Polly are asked to pay a $20/month maintenance fee, regardless of how many projects use it, to help fund the ongoing work of keeping the project secure and maintained; the source code itself stays free and openly licensed. Individuals, students, hobbyists, and organizations under that revenue threshold owe nothing. It&rsquo;s a low bar as these things go, but worth putting on your radar if you&rsquo;re doing a license/dependency audit this year.</p>
<h2 id="migrating-an-existing-codebase">Migrating an existing codebase</h2>
<p>If you&rsquo;re sitting on a codebase full of <code>Policy.Handle&lt;T&gt;().WaitAndRetryAsync(...)</code>, you don&rsquo;t need to rewrite everything at once. A reasonable path:</p>
<ol>
<li><strong>Add <code>Polly.Core</code></strong> alongside your existing <code>Polly</code> reference; the legacy policy API and the new pipeline API can coexist.</li>
<li><strong>Start with your <code>HttpClient</code> calls.</strong> Swap <code>AddPolicyHandler</code> usages for <code>AddStandardResilienceHandler()</code> where the defaults are close enough, and a hand-built <code>AddResilienceHandler</code> where they&rsquo;re not.</li>
<li><strong>Migrate non-HTTP policies pipeline-by-pipeline</strong>, using the mapping above (<code>WaitAndRetryAsync</code> → <code>AddRetry</code>, and so on), rather than trying to preserve <code>PolicyWrap</code> structures, the pipeline builder&rsquo;s ordering already gives you that composition.</li>
<li><strong>Register pipelines centrally</strong> with <code>AddResiliencePipeline</code> so they&rsquo;re easy to find, test, and eventually replace the last of the legacy policy code.</li>
</ol>
<h2 id="wrapping-up">Wrapping up</h2>
<p>The core promise of Polly hasn&rsquo;t changed since it first showed up in .NET projects: don&rsquo;t let a transient failure in one dependency become a permanent failure in yours. What&rsquo;s changed in v8 is that this is no longer something you bolt on with ad-hoc policy objects; it&rsquo;s a composable pipeline with a clear execution order, first-class DI support, ready-made HTTP defaults from Microsoft, hedging for latency-sensitive paths, and chaos strategies to verify the whole thing actually works. If you&rsquo;ve been putting off the v7-to-v8 migration, 2026 is a good year to make the switch. The legacy API isn&rsquo;t getting the new features, and the standard resilience handlers alone are usually worth the move for HTTP-heavy services.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://www.pollydocs.org/migration-v8.html">Migration guide from v7 to v8 — Polly official docs</a></li>
<li><a href="https://www.dotnet-guide.com/tutorials/cloud-native/polly-resilience/">Polly Resilience (Polly v8): Timeouts, Retries, Circuits, Bulkheads, Hedging</a></li>
</ul>
]]></content:encoded></item></channel></rss>