<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>NResilience Blog</title>
    <link>https://nresilience.net/blog</link>
    <description>Deep dives into the architecture of NResilience - exploring single-loop execution, memory budgets, and the low-level trade-offs of building a high-performance resilience engine for .NET.</description>
    <language>en-us</language>
    <lastBuildDate>Thu, 10 Sep 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://nresilience.net/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Break things on purpose - Chaos testing with NResilience</title>
      <link>https://nresilience.net/blog/break-things-on-purpose</link>
      <guid isPermaLink="true">https://nresilience.net/blog/break-things-on-purpose</guid>
      <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
      <description>Stop hoping your resilience policies work. Use the Chaos tool to inject faults through the pipeline and verify your system&apos;s behavior under pressure.</description>

      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Hope is not a resilience strategy. NResilience includes a Chaos tool that lets you inject latency and faults directly into your call path. Because it sits inside your resilience pipeline, it tests whether your classifiers, retries, and breakers actually behave as expected - not just whether your app survives a crash.</p>
<h2>The &quot;Hope&quot; Strategy</h2>
<p>Most developers write resilience policies and then &quot;hope&quot; they work. They might write a unit test that mocks a dependency to throw an exception, but that usually tests the <em>exception handler</em>, not the <em>resilience policy</em>.</p>
<p>If you wrap a call in a retry policy, you don&#39;t just want to know that the app doesn&#39;t crash. You want to know:</p>
<ul>
<li>Does the classifier actually see this specific error as <code>Transient</code>?</li>
<li>Does the retry budget bound the amplification, or are you about to DDoS your own database?</li>
<li>Does the circuit breaker trip exactly when it should, and does it probe for recovery?</li>
</ul>
<p>To answer these, you need to break things on purpose, and you need to do it in a way that exercises the entire pipeline.</p>
<h2>Through-the-Pipeline</h2>
<p>The core philosophy of the NResilience Chaos tool is that faults must traverse the real pipeline.</p>
<p>If you inject a fault <em>outside</em> your resilience call, you are bypassing the very logic you&#39;re trying to test. You&#39;re testing the &quot;outer&quot; world. To test the policy, the fault must be injected <em>inside</em> the callback that the policy executes.</p>
<p>When a fault is injected inside, it is treated exactly like a real failure:</p>
<ol>
<li>It is passed to the <strong>Classifier</strong>.</li>
<li>The resulting <strong>Verdict</strong> determines if it retries.</li>
<li>It is counted against the <strong>Circuit Breaker</strong>.</li>
<li>It consumes the <strong>Retry Budget</strong>.</li>
</ol>
<h2>How to Inject Chaos</h2>
<h3>Wrapping Callbacks</h3>
<p>For general async work, you can wrap your callback using the <code>Inject</code> extension method. This replaces your normal work with a version that occasionally fails or slows down based on a <code>Chaos</code> profile.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">;</span>
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">.</span><span class="tok-type">Testing</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// 1. Define the chaos profile</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> chaos </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">Chaos</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok-punct">{</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Enabled</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">true</span><span class="tok-punct">,</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">FaultRate</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">0.1</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-comment">// 10% of calls fail</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Latency</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMilliseconds</span><span class="tok-punct">(</span><span class="tok-number">500</span><span class="tok-punct">),</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">LatencyRate</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">0.2</span><span class="tok"> </span><span class="tok-comment">// 20% of calls are slowed</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// 2. Wrap your actual work</span>
</span><span class="md-code-line"><span class="tok-method">Func</span><span class="tok-punct">&lt;</span><span class="tok-type">CancellationToken</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-method">Task</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;&gt;</span><span class="tok"> work </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">async</span><span class="tok"> ct </span><span class="tok-punct">=&gt;</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">return</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> _service</span><span class="tok-punct">.</span><span class="tok-method">GetDataAsync</span><span class="tok-punct">(</span><span class="tok">ct</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> chaoticWork </span><span class="tok-punct">=</span><span class="tok"> chaos</span><span class="tok-punct">.</span><span class="tok-method">Inject</span><span class="tok-punct">(</span><span class="tok">work</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// 3. Run it through your policy</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> result </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">chaoticWork</span><span class="tok-punct">,</span><span class="tok"> ct</span><span class="tok-punct">);</span>
</span></code></pre><h3>Chaos in the HTTP Pipeline</h3>
<p>If you&#39;re using <code>HttpClient</code>, the <code>ChaosHandler</code> lets you inject faults into the <code>DelegatingHandler</code> chain. The key is to add it <strong>after</strong> <code>AddResilience()</code>. In .NET&#39;s handler chain, &quot;after&quot; means &quot;inner to,&quot; so the fault is injected <em>before</em> the request leaves the app but <em>after</em> the resilience handler has wrapped it.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok">services</span><span class="tok-punct">.</span><span class="tok-method">AddHttpClient</span><span class="tok-punct">&lt;</span><span class="tok-type">OrdersClient</span><span class="tok-punct">&gt;()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">AddResilience</span><span class="tok-punct">()</span><span class="tok"> </span><span class="tok-comment">// Outer: handles the retries/breakers</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">AddHttpMessageHandler</span><span class="tok-punct">(()</span><span class="tok"> </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">ChaosHandler</span><span class="tok-punct">(</span><span class="tok">chaos</span><span class="tok-punct">));</span><span class="tok"> </span><span class="tok-comment">// Inner: injects the fault</span>
</span></code></pre><h2>Testing Outcomes vs. Exceptions</h2>
<p>Real failures aren&#39;t always exceptions. Sometimes a dependency returns a <code>503 Service Unavailable</code> or a <code>429 Too Many Requests</code>. If your classifier is tuned to retry these specific status codes, testing with <code>throw new Exception()</code> is useless.</p>
<p>The Chaos tool allows you to inject <strong>outcomes</strong> - specific return values that the classifier must judge.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// Inject a 503 response instead of throwing an exception</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> chaosHandler </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">ChaosHandler</span><span class="tok-punct">(</span><span class="tok">chaos</span><span class="tok-punct">,</span><span class="tok"> response</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-punct">()</span><span class="tok"> </span><span class="tok-punct">=&gt;</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">return</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">HttpResponseMessage</span><span class="tok-punct">(</span><span class="tok-type">HttpStatusCode</span><span class="tok-punct">.</span><span class="tok-type">ServiceUnavailable</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-punct">});</span>
</span></code></pre><h2>Game Days and Production Safety</h2>
<p>You might be hesitant to leave testing code in production. However, <code>Chaos</code> is designed to be low-cost. When <code>Enabled = false</code>, the <code>Inject</code> method returns the original callback untouched.</p>
<p>At the call site, this is just one boolean check at composition time. There is zero overhead per call when chaos is off. This means you can safely leave <code>Inject</code> in your production code and toggle a &quot;Game Day&quot; on and off via configuration without redeploying.</p>
<h2>Determinism and Seeds</h2>
<p>Randomness is the enemy of a stable CI build. To make chaos tests repeatable, <code>Chaos</code> accepts a <code>Seed</code>. If you use the same seed and drive your calls sequentially, you will get the exact same sequence of faults every time.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> chaos </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">Chaos</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">Seed</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">42</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-type">FaultRate</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">0.1</span><span class="tok"> </span><span class="tok-punct">};</span>
</span></code></pre><h2>What&#39;s Next?</h2>
<p>Chaos testing is great for verifying that your policies react correctly to single faults. But real-world outages are rarely single events - they are sequences of degradations, partial failures, and recovery cycles.</p>
<p>In our next deep dive, we&#39;ll look at <strong>Simulation</strong>. While Chaos injects random noise, Simulation lets you script precise failure scenarios to see exactly how your system evolves over time.</p>
<hr>
<p><strong>Go deeper:</strong> Read the <a href="https://docs.nresilience.net/testing/fault-injection.md">Fault Injection documentation</a> for a full API reference.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Scripts, not anecdotes - Deep dive into Simulation</title>
      <link>https://nresilience.net/blog/scripts-not-anecdotes</link>
      <guid isPermaLink="true">https://nresilience.net/blog/scripts-not-anecdotes</guid>
      <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
      <description>Stop guessing how your system recovers from an outage. Use scripted simulations to turn resilience behavior into a deterministic, assertable fact.</description>

      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Chaos testing tells you <em>if</em> something breaks; Simulation tells you <em>how</em> it recovers. By using virtual time and scripted sequences, NResilience lets you compress five minutes of failure and recovery into a few milliseconds of CPU time, producing a deterministic report you can actually assert against in CI.</p>
<h2>The Flakiness of &quot;Real Time&quot;</h2>
<p>If you want to test how a circuit breaker recovers from a 30-second outage, you have two bad options:</p>
<ol>
<li><strong>Actually wait 30 seconds.</strong> Your CI pipeline slows to a crawl, and you&#39;ll eventually get a flaky test because a background thread took 1ms too long.</li>
<li><strong>Use a mock clock.</strong> You spend half your day fighting <code>Task.Delay</code> and trying to figure out why your mock doesn&#39;t trigger the actual timer inside the library.</li>
</ol>
<p>Simulation solves this by using <strong>Virtual Time</strong>.</p>
<h2>How Simulation Works</h2>
<p>At the heart of a simulation is a <code>SimulationClock</code>. It&#39;s a <code>TimeProvider</code> that doesn&#39;t track wall-clock time. Instead, it only moves when the driver tells it to move.</p>
<p>When the executor asks to &quot;delay for 10 seconds,&quot; the simulation doesn&#39;t sleep. It queues a timer and immediately jumps the virtual clock forward to the next due event. Five simulated minutes cost whatever the arithmetic costs and no more.</p>
<p>Because everything runs on a single driver thread, there are no races. The sequence of events is identical every single time you run it with the same seed.</p>
<h2>Scripting the Failure</h2>
<p>While Chaos injects random noise, Simulation uses <code>Sequence</code> to define a precise script of outcomes. This allows you to model a specific disaster scenario: &quot;The dependency fails twice, then slows down for three calls, then recovers.&quot;</p>
<h3>Scripting a Callback</h3>
<p>You can script any callback by defining a sequence of returns, throws, and delays.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">.</span><span class="tok-type">Testing</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Script a specific sequence of events</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> calls </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Sequence</span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Throws</span><span class="tok-punct">(</span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">IOException</span><span class="tok-punct">(</span><span class="tok-string">&quot;Connection reset&quot;</span><span class="tok-punct">))</span><span class="tok"> </span><span class="tok-comment">// Call 1: Fail</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Throws</span><span class="tok-punct">(</span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">IOException</span><span class="tok-punct">(</span><span class="tok-string">&quot;Connection reset&quot;</span><span class="tok-punct">))</span><span class="tok"> </span><span class="tok-comment">// Call 2: Fail</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Delays</span><span class="tok-punct">(</span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">2</span><span class="tok-punct">))</span><span class="tok">            </span><span class="tok-comment">// Call 3: Slow...</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Returns</span><span class="tok-punct">(</span><span class="tok-string">&quot;Success!&quot;</span><span class="tok-punct">)</span><span class="tok">                         </span><span class="tok-comment">// ...then succeed</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Returns</span><span class="tok-punct">(</span><span class="tok-string">&quot;Success!&quot;</span><span class="tok-punct">);</span><span class="tok">                        </span><span class="tok-comment">// Call 4: Succeed</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Run this through your policy</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> result </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> calls</span><span class="tok-punct">.</span><span class="tok-method">NextAsync</span><span class="tok-punct">(</span><span class="tok">attempt</span><span class="tok-punct">),</span><span class="tok"> ct</span><span class="tok-punct">);</span>
</span></code></pre><h3>Scripting the HTTP Layer</h3>
<p>For HTTP clients, <code>ScriptedHttpHandler</code> lets you define a sequence of responses without needing a real network transport.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> transport </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">ScriptedHttpHandler</span><span class="tok-punct">()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Responds</span><span class="tok-punct">(</span><span class="tok-type">HttpStatusCode</span><span class="tok-punct">.</span><span class="tok-type">ServiceUnavailable</span><span class="tok-punct">,</span><span class="tok"> times</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">2</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Responds</span><span class="tok-punct">(</span><span class="tok-type">HttpStatusCode</span><span class="tok-punct">.</span><span class="tok-type">OK</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> client </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">HttpResilience</span><span class="tok-punct">.</span><span class="tok-method">CreateClient</span><span class="tok-punct">(</span><span class="tok">policy</span><span class="tok-punct">,</span><span class="tok"> innerHandler</span><span class="tok-punct">:</span><span class="tok"> transport</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> response </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> client</span><span class="tok-punct">.</span><span class="tok-method">GetAsync</span><span class="tok-punct">(</span><span class="tok">uri</span><span class="tok-punct">,</span><span class="tok"> ct</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Assert that we actually made 3 attempts</span>
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">Equal</span><span class="tok-punct">(</span><span class="tok-number">3</span><span class="tok-punct">,</span><span class="tok"> transport</span><span class="tok-punct">.</span><span class="tok-type">CallCount</span><span class="tok-punct">);</span>
</span></code></pre><h2>The Simulation Report: Facts, Not Anecdotes</h2>
<p>The goal of a simulation isn&#39;t just to see if the code finishes; it&#39;s to measure the <strong>impact</strong>.</p>
<p>The <code>SimulationReport</code> captures the total cost of the resilience policy during the run. Instead of saying &quot;it felt like it recovered quickly,&quot; you get hard numbers:</p>
<ul>
<li><strong>Load Multiplier:</strong> Exactly how much extra traffic did the retries send to the dependency? (e.g., 1.1x)</li>
<li><strong>Availability:</strong> What fraction of caller requests actually succeeded?</li>
<li><strong>TimeToRecover:</strong> How long after the impairment ended did the system return to 100% success?</li>
<li><strong>P99 Latency:</strong> What was the worst-case experience for the caller?</li>
</ul>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// Example report output</span>
</span><span class="md-code-line"><span class="tok-comment">// Simulation seed 42 over 00:05:00</span>
</span><span class="md-code-line"><span class="tok-comment">//   Calls            1000</span>
</span><span class="md-code-line"><span class="tok-comment">//   Succeeded        950</span>
</span><span class="md-code-line"><span class="tok-comment">//   Failed            50</span>
</span><span class="md-code-line"><span class="tok-comment">//   Availability     0.9500</span>
</span><span class="md-code-line"><span class="tok-comment">//   LoadMultiplier   1.120</span>
</span><span class="md-code-line"><span class="tok-comment">//   Amplification    1.050</span>
</span><span class="md-code-line"><span class="tok-comment">//   Latency p50      00:00:00.012</span>
</span><span class="md-code-line"><span class="tok-comment">//   Latency p99      00:00:02.450</span>
</span><span class="md-code-line"><span class="tok-comment">//   BreakerOpens     1</span>
</span><span class="md-code-line"><span class="tok-comment">//   TimeToRecover    00:00:04.200</span>
</span></code></pre><p>Because the report is deterministic, you can turn it into a CI gate: <code>Assert.True(report.LoadMultiplier &lt;= 1.2);</code>. If a change to your policy causes a retry storm, the test fails.</p>
<h2>Chaos vs. Simulation</h2>
<p>If you&#39;re wondering which to use:</p>
<ul>
<li><strong>Use Chaos</strong> when you want to smoke-test your system. It&#39;s great for &quot;Game Days&quot; where you want to see if the app survives random instability.</li>
<li><strong>Use Simulation</strong> when you need to prove a specific behavior. It&#39;s for regression testing, tuning timeouts, and asserting that your retry budget actually bounds amplification.</li>
</ul>
<hr>
<p><strong>Go deeper:</strong> Check out the <a href="https://docs.nresilience.net/testing/index.md">Testing documentation</a> to see how to combine <code>Sequence</code> with <code>FakeTimeProvider</code> for deterministic unit tests.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Stress-tested against Polly</title>
      <link>https://nresilience.net/blog/stress-tested-against-polly</link>
      <guid isPermaLink="true">https://nresilience.net/blog/stress-tested-against-polly</guid>
      <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
      <description>I ran NResilience and Polly 8 through a sustained-load stress suite for 32 minutes. Here are the numbers.</description>
      <category>benchmarks</category>
      <category>testing</category>
      <category>methodology</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> I built a stress suite that runs NResilience and Polly 8 under sustained load - 90 configurations, three repetitions each, fresh processes. NResilience used 1.6x-2.4x less memory per call, spent about 2.3x less time paused in GC, and was 1.2x-1.8x faster where nothing retries. Where both libraries actually retry, it was a tie. The retry budget cut retries sent to a failing dependency by 4x-19x. Full report: <a href="https://github.com/nresilience/NResilience/blob/main/bench/stress-results.md"><code>bench/stress-results.md</code></a>.</p>
<h2>Why a stress suite?</h2>
<p>Most benchmarks measure one call at a time: how many bytes, how many nanoseconds. That is useful, but it is not the same as 80 concurrent callers hitting a library against a dependency that is failing.</p>
<p>NResilience already has an allocation gate in CI to catch regressions, and a latency harness to trend per-operation cost. Neither answers the actual operator&#39;s question: what does this process sustain, and what does the dependency feel from the other side?</p>
<p>I built <code>bench/NResilience.Stress</code> to find out. It runs both libraries through the same harness under sustained load and measures throughput, memory, GC pauses, and how much load a guard keeps off a failing dependency. 90 configurations, three repetitions each, 270 runs in fresh processes, about 32 minutes.</p>
<h2>The numbers</h2>
<p>I paired each NResilience configuration with its closest Polly equivalent - same gate, same suspension count, same test callbacks.</p>
<table>
<thead>
<tr>
<th align="left">Question</th>
<th align="left">Result</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>Memory per call</strong></td>
<td align="left">1.6x-2.4x less than Polly. 0% spread across repetitions, byte-identical in a second independent run.</td>
</tr>
<tr>
<td align="left"><strong>Doing nothing</strong></td>
<td align="left">With every feature off, 224 B/call - same as raw code with no library. Polly&#39;s empty pipeline costs 528 B/call.</td>
</tr>
<tr>
<td align="left"><strong>Throughput</strong></td>
<td align="left">1.2x-1.8x faster where nothing retries. When both libraries actually retry: an exact tie.</td>
</tr>
<tr>
<td align="left"><strong>GC pauses</strong></td>
<td align="left">Full default policy allocates at 1.25 GB/s - Polly&#39;s <em>empty</em> pipeline allocates at 1.28 GB/s. Result: 7.6% of time paused in GC vs Polly&#39;s 17.3% at 10 workers.</td>
</tr>
<tr>
<td align="left"><strong>Retry budget</strong></td>
<td align="left">Against a dependency failing half its requests: 19x fewer retries sent, completing 53% as many operations. Failing a quarter: 4x fewer, completing 81%.</td>
</tr>
<tr>
<td align="left"><strong>HTTP handler</strong></td>
<td align="left">1,273 B/request above transport vs a minimal Polly bridge&#39;s 1,209 B. Parity, marginally behind.</td>
</tr>
<tr>
<td align="left"><strong>Tail latency</strong></td>
<td align="left">Not measurable on this machine. No p99 claim is made.</td>
</tr>
</tbody></table>
<p>Read the caveats before the scoreboard convinces you of something the run did not measure. No saturation was tested - 80 workers is high concurrency, not a saturated thread pool. Polly ran its cheapest configuration, pinned at 8.7.0. Throughput ratios moved 10-30% between runs and are quoted as ranges; anything without one should be read as &quot;on this machine.&quot;</p>
<h2>What was surprising</h2>
<p><strong>Deterministic allocations.</strong> I expected noisy medians. Instead: 0% spread across three repetitions, and a second, independent full run byte-for-byte identical. At the floor - every feature off - NResilience costs 224 B/call, exactly what the raw test callback allocates with no library at all. Turning NResilience off is free, byte-for-byte.</p>
<p><strong>The retry tie.</strong> Both libraries at three attempts, zero delay: 1.00x-1.13x, a tie. I expected the retry loop itself to favor the flat executor. It does not - the advantage is in the frame around the call, not the retry loop. The win is not &quot;we retry faster.&quot;</p>
<p><strong>The p99 spread.</strong> Identical repetitions of the same cell produced p99s spanning 1.02 ms to 8.49 ms - a factor of 8 - with GC under 1% of the window. Multi-millisecond stalls of a whole process on a fanless laptop. That is the environment, not the library, and it means this run makes no tail-latency claim.</p>
<p><strong>The budget&#39;s price.</strong> Against a dependency failing half its requests, the budget cut retries-per-operation 18.9x - and completed 53% as many operations, refusing the rest. Client-side availability for load relief on a shared dependency is a good trade across a fleet and a bad one for a single client with no fleet to protect.</p>
<h2>How I kept it honest</h2>
<p>Benchmarks are easy to rig by accident. A few decisions mattered:</p>
<ul>
<li><strong>Fresh process per test</strong>, so GC state belongs to exactly one library.</li>
<li><strong>Shuffled order</strong>, so thermal drift does not land on one library.</li>
<li><strong>Three repetitions, spreads published</strong> - a single run cannot support a claim that is a few percent.</li>
<li><strong>The HTTP dependency in its own process</strong>, so its garbage does not pollute the client&#39;s numbers.</li>
</ul>
<p>One caveat: Polly has no equivalent of the deadline, classifier, or retry budget, so in the &quot;default machinery&quot; pair NResilience is doing more work, not the same work. Both sides also ran their cheapest configuration - no telemetry on either. A claim that survives both of those notes is worth more than one that does not.</p>
<h2>Try it yourself</h2>
<p>The suite ships with the library:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-method">dotnet</span><span class="tok"> run </span><span class="tok-attr">-c</span><span class="tok"> Release </span><span class="tok-attr">--project</span><span class="tok"> bench/NResilience.Stress -- </span><span class="tok-attr">--quick</span><span class="tok">   </span><span class="tok-comment"># one DOP, one rate, single repetition</span>
</span><span class="md-code-line"><span class="tok-method">dotnet</span><span class="tok"> run </span><span class="tok-attr">-c</span><span class="tok"> Release </span><span class="tok-attr">--project</span><span class="tok"> bench/NResilience.Stress             </span><span class="tok-comment"># the full matrix, ~32 min</span>
</span></code></pre><p>Reports land under <code>artifacts/stress/</code> as timestamped Markdown and JSON. The JSON holds <strong>every repetition</strong>, not the medians - so anything asserted in the report can be recomputed from it. Run it on your hardware and the numbers will differ; throughput ratios here moved 10-30% between runs.</p>
<p>One machine, one run: a 10-core Apple Silicon laptop, .NET 10, Polly pinned at 8.7.0.</p>
<p>The full figures are in <a href="https://github.com/nresilience/NResilience/blob/main/bench/stress-results.md"><code>bench/stress-results.md</code></a>. For how the same performance discipline is enforced per-change in CI, <a href="https://nresilience.net/blog/shipping-a-resilience-library">Shipping a resilience library</a> covers the gates. For the executor the suite is measuring, <a href="https://nresilience.net/blog/one-loop-one-allocation">One loop, one allocation</a> explains the flat executor. The library and docs live at <a href="https://docs.nresilience.net">docs.nresilience.net</a> - and if this is useful, a GitHub star helps others find it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A deep dive into the circuit breaker</title>
      <link>https://nresilience.net/blog/deep-dive-circuit-breaker</link>
      <guid isPermaLink="true">https://nresilience.net/blog/deep-dive-circuit-breaker</guid>
      <pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate>
      <description>The breaker state machine, adaptive trips that measure instead of guess, the 100ms guarded rejection, the recovery ramp, and equal jitter - how each piece works and why it is there.</description>
      <category>Deep dive</category>
      <category>deep-dive</category>
      <category>circuit-breaker</category>
      <category>brownouts</category>
      <content:encoded><![CDATA[<p><strong>Summary:</strong> A circuit breaker is not a failure counter with a door on it. NResilience&#39;s breaker measures what your dependency considers normal, trips on deviations from that normal rather than on magic thresholds, refuses calls without melting your CPU, hands traffic back over a ramp, and de-correlates a fleet&#39;s probes with equal jitter. Here is each piece and the reasoning behind it.</p>
<h2>Stop calling it</h2>
<p>When a dependency starts failing, the instinct is to retry until it goes through. If a hundred instances of your app all do that at once, you have not fixed the outage - you have organized a distributed denial of service attack against your own infrastructure. That is how a database hiccup becomes a system-wide outage.</p>
<p>The kindest thing you can do for a struggling dependency is to stop calling it. That is the breaker&#39;s whole job.</p>
<p>The breaker is a state machine with three states you will actually see in operation:</p>
<ul>
<li><em>Closed</em> - normal operation. The circuit is complete, traffic flows, and the breaker quietly samples every attempt.</li>
<li><em>Open</em> - the breaker has tripped. Every call is refused with a <code>CallRejectedException</code> without touching the network. The dependency gets silence, which is the one thing it needs.</li>
<li><em>HalfOpen</em> - after the break duration elapses, a small number of trial calls are let through. One concurrent probe by default. Two successful probes close the breaker; a failed probe re-opens it immediately.</li>
</ul>
<p>That last number is worth pausing on. Closing after a single lucky probe is how you get oscillation: a dependency that is only intermittently available passes one probe just as a fleet of accumulated retries arrives, and the cycle repeats. Two consecutive successes prove the dependency actually recovered.</p>
<p>There is a fourth state, <em>Recovering</em>, but it is opt-in - we get to it later.</p>
<h2>The break doubles</h2>
<p>If a probe fails, the breaker re-opens - and the next break is longer. The break duration doubles on each consecutive open, up to a configured maximum (15 seconds by default, 2 minutes at the top). It is exponential backoff applied to the breaker itself.</p>
<p>This is what bounds a flapping dependency. A service that comes back, accepts its probe, and dies again does not get probed every 15 seconds forever: 15, 30, 60, 120. Unstable dependencies sit in the timeout corner longer, preventing an endless open/half-open loop.</p>
<h2>Tripping without guessing a rate</h2>
<p>The classic breaker asks you for a flat error threshold: trip at 5%. That number cannot fit two dependencies. For a payments API that normally runs at a 0.02% error rate, 5% is a page-everyone catastrophe. For a flaky third-party search backend that has always failed 30% of calls, 5% is a quiet Tuesday. Hardcoding a threshold before the code has seen production traffic is guesswork either way.</p>
<p>NResilience measures instead. By default the breaker tracks the dependency&#39;s own error rate over a rolling 5-minute baseline and trips when the current window&#39;s rate exceeds 5 times that baseline. A payments API spiking to 0.1% - five times its flawless normal - trips, even though no absolute rule would notice. The search backend&#39;s 30% is accepted as its normal; only a significant deterioration trips anything.</p>
<p>Both relative trips come with guards, because a relative measurement against a tiny sample is a way to trip on noise:</p>
<ul>
<li><strong>A floor of 5% absolute.</strong> Below that, nothing is wrong no matter how quiet the baseline was. Five times a baseline of nearly zero is still nearly zero, so without the floor the first error of the day would open the circuit.</li>
<li><strong>At least two failures in the trip window.</strong> One failure is not a rate, it is an event. At a 5% floor and 20 minimum calls, a single transient error would be exactly the threshold - so the relative trip needs a second one.</li>
<li><strong>Enough samples.</strong> The error baseline needs 100 outcomes before the relative trip arms (<code>Failures.MinimumSamples</code>); the latency baseline needs 20 successful calls. Until then the breaker behaves exactly like a consecutive-failures breaker - which is also exactly what it does at cold start, because the consecutive-failure trip is on and needs no history.</li>
</ul>
<p>The consecutive-failure trip (5 by default) is the default&#39;s backbone for a related reason: a rate-based trip needs throughput to mean anything, and a service doing two calls a minute would never arm one. The breaker samples attempts, not logical calls, so a first attempt that trips the breaker stops the second attempt immediately.</p>
<h2>Brownouts: failing slowly</h2>
<p>A dependency does not have to return errors to be broken. A 30-second <code>200 OK</code> is often more dangerous than a 10ms <code>500 Internal Server Error</code>. The hard failure is fast and frees your threads; the brownout ties up your thread pool and connection pool while claiming everything is fine.</p>
<p>The breaker tracks a latency baseline next to the error baseline: the median of successful call duration over the last 5 minutes. An attempt counts as slow when it exceeds 3 times that median, and half the trip window being slow trips the breaker. Nothing here asks you for a millisecond figure.</p>
<p>Why the median and not a high quantile? Because a threshold defined as &quot;the p99 of recent latency&quot; has 1% of calls above it by construction, whatever the dependency does - it can never trip. The median survives a brownout until it accounts for more than half the window it is measured over, and the baseline lives on a 5-minute clock against the trip window&#39;s 30 seconds, so the trip window fills with slow calls long before the baseline forgets what healthy looked like.</p>
<p>The baseline is fed only by successful attempts, and it survives an open, a close, and a <code>Reset</code>. It measures the dependency; it does not judge it. That is what makes the first probe against a still-degraded dependency recognizable as slow - the breaker remembers the pre-brownout latency.</p>
<h2>The refusal that breaks the spin</h2>
<p>Here is a failure mode the breaker itself can cause. A developer wraps a resilient call in a <code>while</code> loop - a critical background job polling until the service is back. The breaker opens, the call is refused... instantly. No network latency, no timeout, just an exception at the speed of a method call. The loop catches it, calls again, gets refused again. Thousands of iterations a second, CPU at 100%.</p>
<p>The mechanism built to shed load has become a load generator - self-inflicted, and invisible on the network dashboards, because outbound traffic is zero. Your system looks safe while the client&#39;s CPU melts.</p>
<p>NResilience fixes this with <em>guarded rejection</em>: a refused call pauses 100ms before returning the refusal. 100ms is imperceptible to a user, but it&#39;s a speed bump that caps a polling loop at 10 iterations a second instead of 10,000.</p>
<p>The pause is not configurable, on purpose: its job is to set a floor under the rate of a rejection loop, not to be a tuning knob. It is bounded by the time remaining on the call&#39;s deadline and observes the cancellation token, so a refusal never pushes a call past the caller&#39;s budget and cancelling aborts it immediately. If you schedule your own retries, <code>CallRejectedException.RetryAfter</code> says when to come back - the break actually being served, not the nominal figure.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">if</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok">result</span><span class="tok-punct">.</span><span class="tok-type">Exception</span><span class="tok"> </span><span class="tok-keyword">is</span><span class="tok"> </span><span class="tok-type">CallRejectedException</span><span class="tok"> rejection</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Console</span><span class="tok-punct">.</span><span class="tok-method">WriteLine</span><span class="tok-punct">(</span><span class="tok">rejection</span><span class="tok-punct">.</span><span class="tok-type">Reason</span><span class="tok-punct">);</span><span class="tok">   </span><span class="tok-comment">// DependencyUnavailable, or BudgetExhausted</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Console</span><span class="tok-punct">.</span><span class="tok-method">WriteLine</span><span class="tok-punct">(</span><span class="tok">rejection</span><span class="tok-punct">.</span><span class="tok-type">RetryAfter</span><span class="tok-punct">);</span><span class="tok"> </span><span class="tok-comment">// when to come back, when there is an answer</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><p>This is the general truth the breaker makes visible: you have to protect the dependency from the client, and the client from its own protective mechanisms.</p>
<h2>Hand the traffic back over a ramp</h2>
<p>Two successful probes prove the dependency can serve two calls. A naive breaker reads that as proof it can serve two thousand, closes, and the entire backed-up load arrives in one millisecond. The recovering dependency has a cold cache and an empty connection pool. It fails, the breaker re-opens with a doubled break, and you have the crash/wait cycle all over again.</p>
<p><code>Recovery</code> adds a transitional <em>Recovering</em> state between half-open and closed. It admits a small fraction of traffic - 5% to start - and refuses the rest. The fraction is the lower of two terms:</p>
<ul>
<li><strong>A clock</strong>, climbing linearly across a ramp whose length is a fraction of the break just served (default 0.25, so a 15-second break ramps over about 4 seconds).</li>
<li><strong>Evidence</strong>, which is AIMD - the same algorithm TCP uses for congestion control. Admitted calls coming back fast add a step, slowly and linearly. A call comes back slow: the admitted fraction is slashed in half on the spot. Aggressive on stress, cautious on recovery.</li>
</ul>
<p>One real failure during the ramp abandons the hypothesis and re-opens the breaker with the grown break. The ramp is opt-in because a call refused during it would have been served by a breaker that closed immediately - but the alternative is not &quot;all served&quot;. It is &quot;all served once, then all refused again for twice as long&quot;.</p>
<h2>The synchronized probe, and equal jitter</h2>
<p>The ramp solves the pod-level cliff. There is a fleet-level problem just before it: 200 pods watching the same dependency all trip within a second of each other, all set the same break, and all fire their one probe in the same second. The recovering gateway receives a synchronized pulse of 200 probes, fails them, and all 200 breakers double their backoff together. Fleet-wide oscillation.</p>
<p>The standard fix is jitter - randomizing the break so the pods do not wake together. But <code>Jitter.Full</code> has a flaw: the random delay could be anywhere from zero to the full break, so a pod could probe 200ms into a 15-second break. The break duration is not just de-correlation - it is the guaranteed rest period the dependency gets.</p>
<p>So NResilience defaults to <code>Jitter.Equal</code>: at least half the break is strictly enforced, and only the second half is random.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> breaker </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">Breaker</span><span class="tok-punct">(</span><span class="tok">settings</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">BreakerSettings</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Name</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-string">&quot;search&quot;</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">BreakDuration</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">15</span><span class="tok-punct">),</span><span class="tok"> </span><span class="tok-comment">// now half of that, plus up to half again</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">BreakJitter</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Jitter</span><span class="tok-punct">.</span><span class="tok-type">Equal</span><span class="tok-punct">,</span><span class="tok">               </span><span class="tok-comment">// the default</span>
</span><span class="md-code-line"><span class="tok-punct">});</span>
</span></code></pre><p>Every pod waits at least 7.5 seconds, no exceptions, then adds a random 0 to 7.5 on top. The probes spread over a wide window, and the rest period survives.</p>
<h2>Who the breaker trips for</h2>
<p>One more operational question: the blast radius. If you wire one global breaker for a dependency, tenant A&#39;s malformed requests trip it, and tenant B - sending perfectly valid traffic - finds the door locked too. Tenant A has weaponized your breaker against tenant B.</p>
<p>NResilience isolates state by default where it can: the HTTP handler scopes breakers per host, so a dead analytics API does not trip calls to the auth API; the gRPC integration scopes per service. For the multi-tenant case specifically, <code>PolicyScope&lt;TKey&gt;</code> gives every tenant, shard, or channel its own breaker, retry budget, and latency estimate:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">private</span><span class="tok"> </span><span class="tok-keyword">static</span><span class="tok"> </span><span class="tok-keyword">readonly</span><span class="tok"> </span><span class="tok-method">PolicyScope</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;</span><span class="tok"> </span><span class="tok-type">Tenants</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">Breaker</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">Breaker</span><span class="tok-punct">()</span><span class="tok"> </span><span class="tok-punct">});</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-type">Tenants</span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">(</span><span class="tok">tenantId</span><span class="tok-punct">).</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> calls</span><span class="tok-punct">.</span><span class="tok-method">NextAsync</span><span class="tok-punct">(</span><span class="tok">attempt</span><span class="tok-punct">),</span><span class="tok"> cancellationToken</span><span class="tok-punct">);</span>
</span></code></pre><p>Tenant A&#39;s failures only accumulate in tenant A&#39;s breaker. One bound to know: the scope holds at most <code>maximumKeys</code> entries (default 1024), because every entry carries a breaker and a budget, and an evicted tenant that returns gets a fresh breaker that does not remember being open.</p>
<h2>What counts as evidence</h2>
<p>One subtlety that keeps all of the above honest: the breaker does not count every non-200 as a failure. Outcomes are classified into verdicts, and only <code>Transient</code> outcomes are evidence against the dependency. A 500 is a genuine failure. A 429 - or a 503 carrying a <code>Retry-After</code> header - is classified <code>Throttled</code>: the dependency is up and defending itself with its own rate limiter. The breaker ignores throttled responses entirely, because tripping on a functioning rate limiter turns a working defense into a hard outage. And <code>Permanent</code> outcomes are usually client-side facts, not dependency health.</p>
<h2>The breaker as a mirror</h2>
<p>One last detail. When a breaker opens, the health check reports <code>Degraded</code>, not <code>Unhealthy</code>. That is deliberate: the dependency is down, but this process is doing exactly what it was designed to do - shedding load and failing fast. Reporting <code>Unhealthy</code> invites Kubernetes to restart the pod, which discards the breaker&#39;s state and unleashes the traffic storm all over again. Restarting the pod does not fix the database.</p>
<p>That visibility is worth more than it first appears. When a breaker opens and a seemingly unrelated part of your application degrades with it, the breaker has done more than stop a crash - it has revealed the true blast radius of your architecture: how it actually works, not how you drew it on the whiteboard. It is worth putting breaker state on a dashboard for that reason alone.</p>
<p>For more information, see <a href="https://docs.nresilience.net/deep-dives/breaker-internals">Breaker internals</a> has the full state machine, the arithmetic behind every default, and the recovery-ramp failure modes.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A deep dive into retries and the retry budget</title>
      <link>https://nresilience.net/blog/deep-dive-retry-budget</link>
      <guid isPermaLink="true">https://nresilience.net/blog/deep-dive-retry-budget</guid>
      <pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate>
      <description>Why attempt limits cannot stop a retry storm, how a token bucket funded by successful traffic can, the two things it exempts, the hint it refuses to overstate, and the nested-retry marker for the loops it cannot see.</description>
      <category>Deep dive</category>
      <category>deep-dive</category>
      <category>retries</category>
      <category>retry-budget</category>
      <content:encoded><![CDATA[<p><strong>Summary:</strong> A retry limit is not a storm defense. NResilience&#39;s budget bounds retries as a fraction of traffic instead, funds them from a token bucket that successful calls feed, refuses amplification without ever refusing a first attempt, exempts the refusals your own process issues, holds half a bucket back for work someone is waiting on, and reports the nested loops no single layer can see. Here is each piece and the reasoning behind it.</p>
<h2>The multiplication nobody sees</h2>
<p>Retries compose multiplicatively. A frontend that allows three retries calls a backend that allows three, which calls a database that allows three: one user action that fails at the bottom becomes 4 x 4 x 4 = 64 database attempts. Each layer&#39;s configuration looks reasonable in isolation - three retries is a modest number - and every layer is blind to the aggregate, because no single layer can see the whole call chain.</p>
<p>This is what makes a retry storm different from ordinary overload. It is not one misbehaving client. It is a fleet of clients, each independently behaving exactly as configured, delivering hammer blows to a dependency at the one moment it can least afford them. The mechanism every layer installed to survive the failure is the thing that finishes the dependency off.</p>
<p>The obvious fix - lower the attempt counts - does not work, and it is worth seeing why before reaching for it. Attempts compose multiplicatively: whatever fixed limit each layer picks, the product across layers grows faster than any of them. Two retries per layer is still 27 attempts at the bottom of a three-hop chain. No per-call number fixes a cross-call problem.</p>
<h2>The fix is a fraction, not a count</h2>
<p>A budget expressed as a fraction of traffic composes differently. If every client independently funds retries at 10% of its successful traffic, fleet-wide amplification is bounded at 1.1 times the original traffic - whatever the topology, however deep the call chain, with no coordination protocol anywhere.</p>
<p>That bound comes from arithmetic, not cooperation. Each client holds its own retries at one-tenth of its own successes, and the fleet&#39;s total is the sum of those ratios - so the fleet&#39;s aggregate amplification is 1.1x too, however the clients differ in size. Google SRE uses the same figure; Finagle and Envoy budget 0.2.</p>
<p>This is why the retry budget is on by default in NResilience and the attempt count is only a maximum. <code>Attempts</code> answers &quot;how many times may this one call retry&quot;. The budget answers &quot;how much traffic may this process add to a struggling dependency&quot; - which is the question the dependency actually cares about.</p>
<h2>The token bucket</h2>
<p>The budget is a token bucket with two sources of tokens and one rule about spending:</p>
<ul>
<li><strong>Deposits.</strong> Every successful attempt deposits <code>fraction</code> tokens - 0.1 by default. Ten successes fund one retry. In steady state, retries can add at most 10% on top of your successful traffic, whatever your traffic is.</li>
<li><strong>A floor rate.</strong> <code>minimumPerSecond</code> (3 per second by default) refills the bucket at a constant rate regardless of traffic, so a quiet client whose successes are too sparse to fund anything can still retry. Set it to zero and the fraction becomes the only source of tokens.</li>
<li><strong>A burst bound.</strong> The bucket&#39;s capacity is ten seconds of the floor rate - 30 tokens at the defaults. This caps the burst a recovering client can spend at once without changing the sustained rate the floor and deposits set.</li>
<li><strong>A full bucket at cold start.</strong> A new process begins with a full bucket. Throttling a fresh deployment&#39;s first retries would penalize deployment rather than a storm, and the budget&#39;s job is the opposite.</li>
</ul>
<p>One charging rule sits above all of this: only retries are charged. The first attempt of every call always executes. The budget is not a rate limiter - it refuses amplification, not traffic. A dependency cannot be starved by its clients&#39; budgets, because no budget can stop the calls the caller actually wanted to make.</p>
<h2>The lock holds arithmetic and nothing else</h2>
<p><code>Deposit</code> runs on every successful attempt, which makes it the one gate the library takes on the success path. Hold one static policy - the shape every application settles into - and all of that traffic funnels through a single lock inside the budget.</p>
<p>So the lock&#39;s critical section is arithmetic and nothing else. The clock read happens before the lock is taken; the timestamp arrives with the caller. A thread can arrive holding a timestamp another thread has already refilled past, and the guard for that already existed for non-monotonic clocks: a refill with no elapsed time accrues nothing rather than rewinding the refill point. The lost accrual is the sub-microsecond gap between two clock reads, which no per-second rate can observe - the lock remains a thin layer of arithmetic on the success path.</p>
<p>It is a small thing, and it is the difference between a bucket that costs nothing on the hot path and one that shows up in a flame graph.</p>
<h2>The hint that stays honest</h2>
<p>When a retry is refused, the call stops with <code>StopReason.BudgetExhausted</code> after the library&#39;s short guarded rejection pause, and <code>CallRejectedException.RetryAfter</code> says when to come back. The number is worth trusting, because of when it is computed.</p>
<p>The executor asks the budget for the hint <em>after</em> serving the 100ms rejection pause, and the hint refills the bucket first. Get that ordering wrong and the answer is the shortfall as it stood 100ms ago - an overstatement, which is the one thing a <code>RetryAfter</code> must not be. A caller that schedules its own polling on the hint gets the wait the floor rate actually needs to accrue the next token, not a padded version of it.</p>
<p>And when the floor is zero, the hint is null. With no floor rate, only successful traffic can refill the bucket, and no one can honestly predict when your next success arrives. The budget would rather say nothing than make something up.</p>
<h2>What the budget exempts</h2>
<p>Two kinds of retry never draw on the bucket, and both exemptions are about protecting the mechanism from itself.</p>
<p><strong>A refusal this process issued.</strong> When your own rate limiter refuses an attempt, the outcome is classified <code>Throttled</code> with a self-imposed bit set - the dependency was never contacted, so the retry costs it nothing. Charging that retry against the budget would let a burst of self-throttling quietly drain the capacity that real transient failures need, and it would drain it at exactly the moment a client is under load, which is when that capacity matters most. So a self-imposed refusal neither spends nor deposits; it is not evidence in either direction.</p>
<p>The breaker needed a subtler version of the same care. Its recording is still called for a self-imposed refusal - it records nothing, but it returns the in-flight probe slot the refused call occupied. Skip the call to save the work and the probe count leaks downward until a half-open breaker has no slots left to give and never recovers.</p>
<p><strong>Half the bucket, for work nobody is waiting on.</strong> A nightly backfill and a checkout draw on the same bucket, and by default the executor cannot tell them apart. But a budget is a fraction of traffic, not a queue - the only way to guarantee capacity for the checkout is refusing to hand the last of it to work nobody is waiting for. A <code>Sheddable</code> call must leave half the bucket behind: while the dependency is healthy, nothing is refused; once the bucket drains, the backfill stops first.</p>
<p>Every other criticality level spends the bucket exactly as before. The refusal is the ordinary one - same pause, same <code>StopReason.BudgetExhausted</code> - because criticality changes <em>when</em> a refusal happens, not what it is.</p>
<h2>Sharing, and where buckets go to die</h2>
<p>Budget state is per process and unshared. There is no coordination between pods, and that is not a defect - the statistical argument from the fraction is what makes coordination unnecessary. But it decides where a bucket can live:</p>
<ul>
<li><code>RetryBudget.Automatic</code>, the default, resolves to a bucket private to each policy instance - or to each key when the policy is scoped. The privacy is the point: it is why a storm against payments cannot throttle retries to search. Policies derived from the same preset deliberately do not share a bucket.</li>
<li><code>RetryBudget.Shared(&quot;payments&quot;)</code> is a process-wide budget looked up by name. Two policies naming the same string share it, and the first caller&#39;s parameters win. Share one across the policies that hit the same dependency, so three clients of the same API do not each get their own 10% allowance.</li>
<li><code>RetryBudget.None</code> is the one spelling of off, and it is right only when the dependency is known not to be shared.</li>
</ul>
<p>The per-process rule has a consequence worth stating bluntly: a budget allocated per <code>HttpClient</code> instance, or resolved from a scoped DI container, is worthless. It is discarded before it observes enough traffic to bound anything. Hold one instance, or name it.</p>
<p>Two lifetime details keep the mechanism intact where you would expect it to break. In DI, the bucket is pinned to the registration name, so a configuration reload creates a new policy instance but keeps the traffic history. And the bucket an <code>Automatic</code> marker resolves to lives in a <code>ConditionalWeakTable</code> keyed by policy identity, so lazy initialization does not affect record equality - two equal policies still compare equal after one of them has warmed a bucket.</p>
<h2>Reading the budget</h2>
<p>A budget near capacity is the library telling you it is preventing a storm right now:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">if</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok">result</span><span class="tok-punct">.</span><span class="tok-type">Exception</span><span class="tok"> </span><span class="tok-keyword">is</span><span class="tok"> </span><span class="tok-type">CallRejectedException</span><span class="tok"> rejection</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Console</span><span class="tok-punct">.</span><span class="tok-method">WriteLine</span><span class="tok-punct">(</span><span class="tok">rejection</span><span class="tok-punct">.</span><span class="tok-type">Reason</span><span class="tok-punct">);</span><span class="tok">     </span><span class="tok-comment">// BudgetExhausted</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Console</span><span class="tok-punct">.</span><span class="tok-method">WriteLine</span><span class="tok-punct">(</span><span class="tok">rejection</span><span class="tok-punct">.</span><span class="tok-type">RetryAfter</span><span class="tok-punct">);</span><span class="tok"> </span><span class="tok-comment">// when the floor funds the next token</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><p><code>Utilization</code> reads 0 to 1 for dashboards. A budget sitting near 1 is a client whose retries are being refused - a symptom to alert on, not a steady state, because it means your dependency is failing and the budget is absorbing what would otherwise be your contribution to the incident. The ratio of <code>nresilience.attempts</code> to <code>nresilience.calls</code> is the same story from the other side: it is the retry fraction you actually ran, and it is the one number that says whether the amplification you designed for is the amplification you got.</p>
<h2>The loop no single layer can see</h2>
<p>The budget bounds what this process spends. The other half of the amplification problem is structural: your frontend retries onto a backend that retries onto a database, and every layer sees only its own three attempts - the same blindness that makes fixed limits useless.</p>
<p>NResilience&#39;s HTTP handler detects nesting two ways: a flag in the execution context catches it within a process, and an HTTP header catches it across process boundaries. The handler stamps <code>X-NResilience-Retrying: 1</code> on every request it can retry, and the stamp is on the <em>first</em> attempt, not the retries - because by the time a retry goes out, the amplification has already happened. The gRPC interceptor carries the same marker as <code>x-nresilience-retrying: 1</code>, because the chain that amplifies is rarely all one protocol.</p>
<p>In an ASP.NET Core app, receiving the marker is one line:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">UseResilienceNestedRetry</span><span class="tok-punct">();</span>
</span></code></pre><p>Registered before anything that makes an outbound call, it publishes the flag this request carries, so the middle hop of a chain reports <code>NestedRetry</code> events itself and can see an amplification it is part of.</p>
<p>Detection reports and does not intervene: silently dropping configured retries would be surprising and hard to debug. The right response is architectural - the layer closest to the failing dependency should do most of the retrying, and the layers above it should pass the call through. The library finds the loops; you decide what they mean.</p>
<h2>The pacing around it</h2>
<p>The budget decides <em>whether</em> a retry happens. The backoff decides <em>when</em>, and its defaults each carry a reason:</p>
<ul>
<li><strong>Two bases, because two verdicts.</strong> A <code>Transient</code> failure waits 100ms before the first retry; a <code>Throttled</code> one waits 1s. A delay tuned for connection resets is too aggressive for a rate limiter - a 429 is the dependency telling you about its token bucket, not about how long its queue is.</li>
<li><strong>Full jitter, always on.</strong> A brief outage ends and every client&#39;s accumulated retries arrive together - a second traffic spike on top of the recovery. Jitter removes the correlation; without it, a fleet turns a two-second blip into a thundering herd.</li>
<li><strong>Server pushback wins verbatim.</strong> A <code>Retry-After</code> header on a 429 or 503 replaces the computed delay exactly - no jitter, no curve - capped only by the configured maximum and the remaining deadline. Where the server knows, it says so.</li>
<li><strong>The measured base, opt-in.</strong> <code>Backoff.Measured(1)</code> sets the transient base to one recent normal call&#39;s duration instead of a millisecond count that is only right for one dependency. It is the one measured term the library does not turn on for you, because it is the one that can lengthen a delay rather than only shorten one - so it is clamped to a factor of 10 either side of the constant you wrote, and the constant stays the anchor.</li>
</ul>
<h2>The trade, priced</h2>
<p>None of this is free. A budget is a claim that surviving a dependency&#39;s struggles matters more than the availability your retries would have bought during them - and that trade can be measured before shipping. A five-minute simulation of a dependency that goes eight times slower for one minute, at 500 calls a second, puts the worst second the dependency feels at 2.1x load with the budget off and 1.1x with it on, for six points of this client&#39;s availability.</p>
<p>That is the whole idea in two numbers. The budget converts your worst second into the dependency&#39;s survivable one, and it is on by default because the alternative - every client independently being reasonable - is how the storm starts.</p>
<p>For more information, see <a href="https://docs.nresilience.net/deep-dives/retry-budget-internals">Retry budget internals</a>, which has the full token-bucket mechanism and the reasoning for bounding by traffic rather than by attempts.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Beyond the basics: The road to v1.0</title>
      <link>https://nresilience.net/blog/beyond-the-basics</link>
      <guid isPermaLink="true">https://nresilience.net/blog/beyond-the-basics</guid>
      <pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate>
      <description>A final synthesis of the NResilience series, reflecting on the journey from a single retried call to a high-performance resilience engine.</description>
      <category>Wrapping up</category>
      <category>series</category>
      <category>roadmap</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> We have moved from the &quot;get a working call&quot; basics to the internals of the flat executor and CI quality gates. Resilience isn&#39;t a feature you add; it&#39;s a discipline of bounding failure. This final post synthesizes the core lessons of the series and outlines the path toward v1.0.</p>
<h2>The Resilience Journey: A Synthesis</h2>
<p>Over the last 26 posts, we have explored the anatomy of a resilient system. If you started at post 1, you&#39;ve seen the progression from a simple <code>HttpResilience.CreateClient()</code> call to the complex machinery of live latency quantiles and Native AOT probes.</p>
<p>The overarching theme is <strong>bounding</strong>. </p>
<p>Software fails when it is unbounded. A request that can hang for 30 seconds is an unbounded resource. A retry loop that multiplies traffic by 10x during an outage is an unbounded load. A memory allocation that grows with every layer of a pipeline is an unbounded cost.</p>
<p>NResilience is a tool for stating those bounds as plain values and letting a single, flat executor enforce them. Whether it is a <code>Deadline</code> capping the whole call or a <code>RetryBudget</code> bounding fleet-wide amplification, the goal is the same: ensure that when a dependency fails, your application degrades gracefully instead of crashing.</p>
<h2>The Core Pillars of NResilience</h2>
<p>If you take away only three things from this series, let them be these:</p>
<h3>1. Policies are Values</h3>
<p>Resilience should be a configuration, not a complex builder chain. By treating policies as immutable values that you derive with <code>with</code>, the library removes the &quot;wrong ordering&quot; bugs that plague layered architectures.</p>
<h3>2. Measured, Not Guessed</h3>
<p>The most dangerous constant in a system is a &quot;guessed&quot; timeout. By using measured bounds - where the library adapts to actual latency quantiles - you get the protection of tight timeouts without the fragility of hard-coded constants.</p>
<h3>3. Correctness is a CI Gate</h3>
<p>Performance and correctness cannot be &quot;hope-driven.&quot; By enforcing allocation budgets and AOT compatibility as blocking CI gates, we ensure that the library&#39;s promises are backed by evidence, not assertions.</p>
<h2>What&#39;s Next: The Road to v1.0</h2>
<p>NResilience is an evolving project. While the core engine is stable, the journey to v1.0 involves hardening what is here and widening what it covers.</p>
<p><strong>Coming Soon:</strong></p>
<ul>
<li><strong>More protocol coverage:</strong> Deepening the gRPC integration and evaluating support for message brokers.</li>
<li><strong>More adaptive defaults:</strong> Refining the <code>Automatic</code> budget and limiter settings based on real-world usage patterns.</li>
<li><strong>More docs and guides:</strong> The docs gate keeps every snippet compiling and running; the next push is breadth, not correctness.</li>
</ul>
<h2>Final Word: The Discipline of Failure</h2>
<p>The most important lesson of this series isn&#39;t about a specific API or a memory budget. It&#39;s the realization that <strong>failure is a first-class citizen of distributed systems.</strong></p>
<p>The goal isn&#39;t to build a system that never fails - that&#39;s impossible. The goal is to build a system that knows exactly how it is allowed to fail, how much it is allowed to retry, and when it must simply stop.</p>
<p>Thank you for following along with the NResilience series. Now, go bound something.</p>
<p>The <a href="https://github.com/nresilience/NResilience">GitHub Repository</a> has the full source, CI pipeline, and contribution guide if you want to dive in.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Shipping a resilience library</title>
      <link>https://nresilience.net/blog/shipping-a-resilience-library</link>
      <guid isPermaLink="true">https://nresilience.net/blog/shipping-a-resilience-library</guid>
      <pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate>
      <description>A meta-look at the quality gates, analyzer bundling, and CI processes that ensure NResilience&apos;s promises of performance and correctness.</description>
      <category>Under the hood</category>
      <category>ci</category>
      <category>release</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Shipping a library that promises &quot;measurable performance&quot; and &quot;correctness&quot; requires more than just a test suite. This post reveals the internal machinery of NResilience: the five CI gates, the decision to bundle analyzers internally, the API manifests that make surface changes reviewed diffs, and a documentation pipeline that treats code samples as executable tests.</p>
<h2>The Promise vs. The Proof</h2>
<p>When a library claims to be &quot;allocation-free&quot; or &quot;correct by design,&quot; those are bold assertions. In a distributed system, a tiny regression in a core loop can cause a p99 spike that takes down a production cluster.</p>
<p>To move from &quot;asserted&quot; performance to &quot;measured&quot; performance, you cannot rely on manual benchmarks. You need a system of gates that make it impossible to merge a regression.</p>
<h2>The Five Gates of CI</h2>
<p>NResilience&#39;s CI pipeline isn&#39;t just about &quot;passing tests.&quot; It is a series of five specialized gates that must be cleared before any code hits the main branch.</p>
<h3>1. The Behavior Gate</h3>
<p>The standard functional suite. It ensures the logic is correct across thousands of permutations of retries, deadlines, and breakers.
<code>dotnet test tests/NResilience.Tests/...</code></p>
<h3>2. The Allocation Gate</h3>
<p>This is the most critical gate for performance. We use <code>GC.GetAllocatedBytesForCurrentThread()</code> in Release builds to enforce strict memory ceilings. If a change adds even a few bytes to the core execution path, the build fails.
<code>dotnet test tests/NResilience.Gates/...</code></p>
<h3>3. The Integration Gate</h3>
<p>Real-world verification. This gate runs loopback tests against actual HTTP and gRPC listeners to ensure the library survives the &quot;wire&quot; (TCP resets, timeouts, and partial writes).
<code>dotnet test tests/NResilience.IntegrationTests/...</code></p>
<h3>4. The Analyzer Gate</h3>
<p>Since the library provides diagnostics to help users avoid bugs, the diagnostics themselves must be bug-free. This gate tests the analyzers against a suite of broken and fixed code samples.
<code>dotnet test tests/NResilience.Analyzers.Tests/...</code></p>
<h3>5. The Native AOT Gate</h3>
<p>To guarantee AOT compatibility, we don&#39;t just check for warnings. We publish a native binary and execute it. If the published binary crashes or fails to bind configuration, the gate closes.
<code>dotnet publish ... -p:PublishAot=true</code></p>
<h2>Bundling the Intelligence</h2>
<p>A common pattern for .NET libraries is to ship analyzers in a separate NuGet package (e.g., <code>NResilience.Analyzers</code>). We rejected this.</p>
<p>A diagnostic that isn&#39;t installed is a diagnostic that doesn&#39;t exist. If a user misses the analyzer package, they will ship a policy with a 30s deadline and a 90s retry loop, and it will fail silently in production.</p>
<p>By bundling the analyzers internally in the main package (<code>analyzers/dotnet/cs</code>), we ensure that the moment you add the library, the compiler starts reviewing your resilience design. The diagnostics travel with the code.</p>
<h2>API Manifests and Pinned Dependencies</h2>
<p>To prevent &quot;API drift,&quot; NResilience uses manifest-based reviews. Every change to the public surface is tracked in <code>PublicAPI.Shipped.txt</code>. When a dev changes a method signature, they must update the manifest. This turns a casual code change into a deliberate architectural decision during the PR review.</p>
<p>We apply the same rigor to dependencies via <code>Directory.Packages.props</code>:</p>
<ul>
<li><strong>Competitive Baselines:</strong> Dependencies like <code>Polly.Core</code> are pinned. We only bump them when we have re-measured our performance claims against the new version - which is what the sustained-load stress suite in <code>bench/NResilience.Stress</code> is for.</li>
<li><strong>Lockstep Pinning:</strong> All <code>Grpc.*</code> packages are pinned to the exact same version to prevent the notorious &quot;mixed version&quot; crashes common in gRPC-dotnet.</li>
<li><strong>Compatibility Pinning:</strong> Roslyn (<code>Microsoft.CodeAnalysis</code>) is pinned low to ensure our analyzers load in as many compiler versions as possible.</li>
</ul>
<h2>The Docs-to-Code Pipeline</h2>
<p>The most frustrating part of using a library is a code sample in the docs that doesn&#39;t compile. </p>
<p>NResilience treats documentation as code. Every snippet in our markdown files is actually extracted from a real <code>.cs</code> file in <code>tests/NResilience.Docs</code>, marked up with <code>snippet:</code> tags.</p>
<p>The pipeline works like this:</p>
<ol>
<li><strong>Verification:</strong> CI runs <code>dotnet test</code> on the docs project. If a sample fails, the build fails.</li>
<li><strong>Extraction:</strong> A custom tool extracts code between <code>// &lt;snippet:name&gt;</code> markers.</li>
<li><strong>Synchronization:</strong> The tool scans the markdown for matching tags. If the markdown content differs from the source code, the CI fails with a &quot;drift&quot; error.</li>
</ol>
<p>This ensures that the documentation is never a &quot;best guess&quot; - it is a verified, executable truth.</p>
<h2>Conclusion</h2>
<p>Shipping a resilience library is a lesson in humility. You are building the safety net for other people&#39;s systems. By treating performance as a CI gate, analyzers as a core product, and documentation as executable code, we ensure that NResilience&#39;s promises are backed by evidence.</p>
<p>The <a href="https://github.com/nresilience/NResilience">GitHub Repository</a> has the full CI pipeline and source. For the sustained-load evidence behind the allocation and GC claims - a full NResilience-vs-Polly run with every repetition published - <a href="https://nresilience.net/blog/stress-tested-against-polly">Stress-tested against Polly</a> is the summary. To close the series, <a href="https://nresilience.net/blog/beyond-the-basics">Beyond the basics</a> synthesizes the core lessons and outlines the road to v1.0.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The small decisions that add up</title>
      <link>https://nresilience.net/blog/the-small-decisions-that-add-up</link>
      <guid isPermaLink="true">https://nresilience.net/blog/the-small-decisions-that-add-up</guid>
      <pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate>
      <description>An essay on the deliberate design choices in NResilience, from guarded rejections to live quantiles and the anatomy of a Verdict.</description>
      <category>Under the hood</category>
      <category>design</category>
      <category>internals</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Great software isn&#39;t just about the big architecture; it&#39;s about the small, defensive decisions that prevent catastrophic failures. This post explains the &quot;hidden&quot; logic in NResilience: why a tripped breaker pauses for 100ms, how &quot;self-imposed&quot; throttles protect your retry budgets, and why live quantiles are the only safe way to hedge.</p>
<h2>The Philosophy of the Small Decision</h2>
<p>When you build a resilience library, you aren&#39;t just writing code to handle errors; you are writing code that manages the relationship between two systems under stress. In that environment, &quot;standard&quot; logic often fails. </p>
<p>A simple <code>while</code> loop for retries is fine for a demo, but in production, that loop can become a weapon. If not carefully guarded, a resilience library can accidentally amplify a brownout into a total outage. To prevent this, NResilience is built on a series of small, deliberate decisions - most of them invisible to the user - that ensure the library remains a protector, not a load generator.</p>
<h2>Guarded Rejection: Breaking the Spin</h2>
<p>When a circuit breaker trips, it enters the <code>Open</code> state. In a naive implementation, every call to an open breaker returns a &quot;Failed&quot; result immediately.</p>
<p>This sounds efficient, but it creates a dangerous edge case: the <strong>CPU Spin</strong>. If a developer accidentally puts a resilient call inside a tight <code>while</code> loop (or a polling mechanism) without any delay, an open breaker will return thousands of failures per second. This consumes 100% of the CPU, starving other parts of the application and potentially crashing the process.</p>
<p>NResilience implements <strong>Guarded Rejection</strong>. When a breaker is open, the library pauses for 100ms before reporting the refusal. The pause is capped by whatever is left on the deadline and observes your cancellation token, so a refusal never pushes a call past the caller&#39;s budget, and cancelling during the pause aborts immediately.</p>
<p>This pause is non-configurable on purpose. It exists to set a floor under the rate of a rejection loop, not to be a tunable performance parameter. It is short enough that a human user won&#39;t notice it, but long enough to make a CPU spin impossible. It turns a potential crash into a manageable delay. For callers who manage their own retries, <code>CallRejectedException.RetryAfter</code> says when to come back.</p>
<h2>Admission Control and the &quot;Self-Imposed&quot; Bit</h2>
<p>In NResilience, every attempt ends with a <code>Verdict</code>. Whether it&#39;s a 503 from a server or a timeout from the library, the outcome is mapped to a standard verdict (<code>Ok</code>, <code>Transient</code>, <code>Throttled</code>, or <code>Permanent</code>).</p>
<p>But there is a critical distinction when the failure comes from <em>inside</em> the library - for example, when a <code>ConcurrencyLimiter</code> refuses a permit. </p>
<p>If a limiter refuses a call, NResilience classifies it as <code>Verdict.Throttled</code> with a <strong>self-imposed</strong> flag. This distinction is vital for two reasons:</p>
<ol>
<li><strong>The Breaker ignores it:</strong> If your own limiter is refusing calls because you&#39;ve hit your concurrency cap, that isn&#39;t a sign that the dependency is failing. It would be a bug if your local limiter tripped your circuit breaker.</li>
<li><strong>The Budget ignores it:</strong> Retry budgets are designed to prevent you from overwhelming a struggling dependency. If you are throttled by your own internal limiter, you aren&#39;t adding load to the server. Therefore, self-imposed refusals are exempt from the retry budget.</li>
</ol>
<p>The executor enforces this from the inside: a <code>RateLimitedException</code> is always classified as self-imposed, and no classifier rule can turn a refusal this process imposed on itself into evidence against the dependency. Value equality follows the flag too - <code>Verdict.Throttled()</code> and <code>Verdict.Limited()</code> carry the same kind but are not equal.</p>
<p>By distinguishing between &quot;the server is full&quot; and &quot;I am full,&quot; NResilience ensures that your resilience settings don&#39;t fight each other.</p>
<h2>Why Live Quantiles are the Only Safe Hedge</h2>
<p>Hedging - sending a duplicate request when the first one is slow - is the sharpest tool for killing tail latency (p99). It is also the easiest to misuse.</p>
<p>The classic approach is a fixed delay: &quot;If the call takes more than 200ms, send a second one.&quot; This is a trap. If your dependency slows down and every call starts taking 210ms, you have just doubled your traffic to a service that is already struggling. You&#39;ve turned a brownout into a retry storm.</p>
<p>NResilience uses a <strong>live latency quantile</strong> (e.g., p95). Instead of a fixed 200ms, the library tracks the actual latency of recent calls. If the p95 is 10ms, the hedge fires at 10ms. If the dependency slows and the p95 moves to 2s, the hedge fires at 2s.</p>
<p>Because the threshold moves <em>with</em> the traffic, the fraction of calls that hedge remains constant (roughly 5%). The load on the dependency is stable, regardless of whether it is responding in milliseconds or seconds.</p>
<h2>Data-Driven Design: Verdicts and Events</h2>
<p>To keep the library performant, the internal data structures are stripped to the bone.</p>
<p>The <code>Verdict</code> is a 16-byte struct. It has to be: a verdict is live across the attempt <code>await</code>, so every byte it carries is paid in the state-machine box of every suspending call in the library, whether or not anything is ever throttled. That is why the self-imposed flag shares the kind&#39;s byte rather than sitting beside it as a <code>bool</code>, and why <code>RetryAfter</code> is a biased <code>long</code> rather than a <code>TimeSpan?</code> - each packing saves eight bytes for everyone.</p>
<p>Furthermore, NResilience follows a strict <strong>&quot;One Event Per Call&quot;</strong> rule. While a call might have five attempts, you only ever receive one terminal <code>CallEvent</code>. This event summarizes the entire journey: the final verdict, the number of retries, and the total delay. This prevents your telemetry and logs from being flooded by &quot;Attempt X started&quot; noise, giving you a clean, high-signal view of your system&#39;s health.</p>
<h2>Conclusion</h2>
<p>Resilience is not about adding more features; it&#39;s about removing ways for the system to fail. Guarded rejections, self-imposed throttles, and live quantiles are small decisions, but they are the difference between a library that &quot;works in a demo&quot; and a library that survives a production outage.</p>
<p>The <a href="https://docs.nresilience.net/features/classification">Classification and Verdicts</a> guide documents every outcome. For the story of how these decisions are enforced as the library ships, <a href="https://nresilience.net/blog/shipping-a-resilience-library">Shipping a resilience library</a> covers the five CI gates, analyzer bundling, and the docs-to-code pipeline.</p>
]]></content:encoded>
    </item>
    <item>
      <title>One loop, one allocation</title>
      <link>https://nresilience.net/blog/one-loop-one-allocation</link>
      <guid isPermaLink="true">https://nresilience.net/blog/one-loop-one-allocation</guid>
      <pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
      <description>A deep dive into the NResilience flat executor architecture, its allocation budgets, and why a single loop is faster than a layered pipeline.</description>
      <category>Under the hood</category>
      <category>performance</category>
      <category>internals</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Most resilience libraries use a layered pipeline of strategies, where every layer adds a new state machine and new allocations. NResilience uses a &quot;flat executor&quot; - a single async loop that handles retries, deadlines, and breakers in one frame. This reduces the per-call overhead to a constant floor, regardless of policy complexity, and enables full Native AOT support.</p>
<h2>The Problem: The Layered Tax</h2>
<p>In a traditional resilience pipeline, you stack strategies like a cake: <code>Retry</code> -&gt; <code>Timeout</code> -&gt; <code>Circuit Breaker</code> -&gt; <code>Callback</code>. </p>
<p>In .NET, every <code>async</code> method that suspends (waits) heap-allocates a state machine &quot;box&quot; to track its progress. In a layered architecture, each strategy is its own async method. This means if you have three layers of resilience, you pay the allocation tax three times for every single call. </p>
<p>As you add more complex policies - hedging, adaptive limits, or nested retries - the allocation cost grows linearly. This creates GC pressure that manifests as tail-latency spikes (p99), which is exactly what resilience libraries are supposed to prevent.</p>
<h2>The Solution: The Flat Executor</h2>
<p>NResilience rejects the pipeline model. Instead of a chain of strategies, it uses a <strong>Flat Executor</strong>.</p>
<p>The core of the library is a single <code>async</code> loop. This loop handles the entire lifecycle of the call - admission, deadline checks, attempt looping, backoff, and classification - inside one method frame.</p>
<h3>One Frame, One Box</h3>
<p>Because there is only one main <code>async</code> method, there is only one state-machine allocation per call. Whether your policy is a simple retry or a complex combination of adaptive limits, budgets, and deadlines, the overhead remains constant.</p>
<p>The cost is not based on how many <em>settings</em> you have, but on the <em>execution path</em> taken.</p>
<h3>Measured Allocation Budgets</h3>
<p>We don&#39;t guess about performance; we measure it in CI. NResilience enforces strict allocation ceilings using <code>GC.GetAllocatedBytesForCurrentThread()</code> in Release builds.</p>
<table>
<thead>
<tr>
<th>Execution Path</th>
<th>Allocation Cost</th>
<th>Why?</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>Resilience.None</code></strong></td>
<td>0 B</td>
<td>The executor returns the callback&#39;s own task.</td>
</tr>
<tr>
<td><strong>Sync-completing, full policy</strong></td>
<td>72 B</td>
<td>The &quot;floor&quot;: one linked <code>CancellationTokenSource</code> per attempt, because handing out the pooled source&#39;s own token is a data race.</td>
</tr>
<tr>
<td><strong>Suspending, full policy</strong></td>
<td>448 B</td>
<td>The state-machine box for a call that actually suspends.</td>
</tr>
<tr>
<td><strong>Hedged call (no hedge fired)</strong></td>
<td>~1,370 B</td>
<td>The &quot;hedge tax&quot;: tracking multiple in-flight legs.</td>
</tr>
</tbody></table>
<p>By &quot;quarantining&quot; the higher cost of hedging, standard callers never pay for features they aren&#39;t using. If you don&#39;t hedge, you stay at the floor. Every figure is a ceiling enforced by CI, with roughly 15% headroom for cross-architecture variation - and NResilience deliberately does not claim to be &quot;zero allocation&quot; in an unqualified sense. The claim is one box instead of one box per layer, and the build gates enforce it.</p>
<p>These budgets hold under sustained load, too. The stress suite measured a whole policy end to end against Polly: with every feature off, NResilience allocates 224 B/call - byte-for-byte what the raw test callback allocates with no library at all - while Polly&#39;s empty pipeline adds 304 B. The full default policy sits at 624 B/call, and the process spends about 2.3x less time paused in GC than Polly&#39;s retry + timeout pipeline. The full run, with every repetition published, is in <a href="https://nresilience.net/blog/stress-tested-against-polly">Stress-tested against Polly</a>.</p>
<h2>Native AOT and the AOT Probe</h2>
<p>The flat executor&#39;s design makes it a natural fit for Native AOT. Layered pipelines often rely on reflection or complex generic compositions that can trip up AOT compilers or lead to oversized binaries.</p>
<p>NResilience is reflection-free. To ensure this stays true, our CI pipeline includes an <strong>AOT Probe</strong>:</p>
<ol>
<li>It publishes the library with <code>PublishAot=true</code>.</li>
<li>It treats all AOT warnings as errors.</li>
<li>It executes the resulting native binary to verify that DI, configuration binding, and telemetry still work.</li>
</ol>
<p>This means you can ship NResilience in a tiny, fast-starting native binary without worrying about runtime crashes due to missing metadata.</p>
<h2>The Trade-off: Composition vs. Performance</h2>
<p>The cost of the flat executor is a loss of &quot;compositional extensibility.&quot; In a pipeline library, you can write a custom <code>IStrategy</code> class and plug it into the middle of the chain. In NResilience, you cannot.</p>
<p>To solve this without breaking the flat loop, we provide <strong>Extension Hooks</strong>. These are specific, high-leverage points where you can inject logic without adding new async frames:</p>
<ul>
<li><strong><code>Admit</code></strong>: A hook to decide if a call should proceed before the attempt starts.</li>
<li><strong><code>BeforeAttempt</code></strong>: Setup logic that runs before every attempt, including the first; its time counts toward the deadline.</li>
<li><strong><code>OnEvent</code></strong>: A stateless, allocation-free listener for logging and telemetry.</li>
<li><strong><code>Backoff.Custom</code></strong>: A delegate to compute the delay between attempts.</li>
</ul>
<p>You get the customization you need, but the library keeps the performance of a single loop.</p>
<h2>Conclusion</h2>
<p>Resilience should not come at the cost of performance. By replacing the layered pipeline with a flat executor, NResilience ensures that adding safety doesn&#39;t add latency. One loop, one allocation, and zero reflection.</p>
<p>The flat executor exposes specific extension hooks instead of a general strategy pipeline. <a href="https://nresilience.net/blog/bending-the-library">Bending the library</a> covers <code>Admit</code>, <code>BeforeAttempt</code>, <code>Backoff.Custom</code>, and <code>OnEvent</code> - the targeted escape hatches that let you inject custom logic without breaking the performance model.</p>
]]></content:encoded>
    </item>
    <item>
      <title>gRPC in depth</title>
      <link>https://nresilience.net/blog/grpc-in-depth</link>
      <guid isPermaLink="true">https://nresilience.net/blog/grpc-in-depth</guid>
      <pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate>
      <description>Explore how NResilience handles gRPC status classification, per-service scoping, and the critical math behind wire deadlines.</description>
      <category>Per stack</category>
      <category>grpc</category>
      <category>deadlines</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> gRPC requires a different approach to resilience than REST. This post covers how NResilience maps RPC status codes to verdicts, implements per-service circuit breakers, manages <code>grpc-timeout</code> with a deliberate slack, and handles the unique constraints of server streaming.</p>
<h2>The Problem: gRPC isn&#39;t just HTTP</h2>
<p>If you look at a network trace, every gRPC call is an HTTP/2 POST. But at the application level, the semantics are entirely different. You don&#39;t have status codes like 404 or 503 in the response header; you have a trailing <code>grpc-status</code>.</p>
<p>This creates several technical challenges:</p>
<ol>
<li><strong>Blast Radius:</strong> A single gRPC client often connects to a server that hosts ten different services. If the &quot;Payments&quot; service is down, you don&#39;t want to trip the breaker for the &quot;Catalog&quot; service.</li>
<li><strong>The Timer Race:</strong> The gRPC stack has its own internal deadline management. If you set a timeout in the library and a timeout on the wire to the exact same millisecond, you have a race condition. You don&#39;t know who won, and your telemetry becomes a lie.</li>
<li><strong>Streaming State:</strong> Unlike a unary call, a stream is a long-lived conversation. If a stream fails after the client has already received five messages, you can&#39;t just &quot;retry&quot; the call - you&#39;d be asking the server to start the whole stream from the beginning.</li>
</ol>
<h2>The Solution: gRPC-Aware Resilience</h2>
<p>NResilience integrates directly into the gRPC channel via a <code>ResilienceInterceptor</code>, allowing it to understand the specific needs of RPCs.</p>
<h3>1. Per-Service Scoping</h3>
<p>By default, NResilience does not use a global breaker for the entire client. Instead, it scopes guards by the <strong>gRPC Service</strong>.</p>
<p>If you are calling <code>OrdersService</code> and <code>InventoryService</code> on the same host, they each get their own circuit breaker and retry budget. This prevents a failure in one domain from taking down your entire integration.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// Registered on the builder that AddGrpcClient&lt;T&gt;() returns</span>
</span><span class="md-code-line"><span class="tok">services</span><span class="tok-punct">.</span><span class="tok-method">AddGrpcClient</span><span class="tok-punct">&lt;</span><span class="tok-type">Orders</span><span class="tok-punct">.</span><span class="tok-type">OrdersClient</span><span class="tok-punct">&gt;(</span><span class="tok">o </span><span class="tok-punct">=&gt;</span><span class="tok"> o</span><span class="tok-punct">.</span><span class="tok-type">Address</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">Uri</span><span class="tok-punct">(</span><span class="tok-string">&quot;https://orders.internal:5001&quot;</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">AddGrpcResilience</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Default behavior:</span>
</span><span class="md-code-line"><span class="tok-comment">// ScopeBy = static m =&gt; m.ServiceName;</span>
</span></code></pre><p>You can tighten this to per-method scoping if you have a specific RPC that is notoriously flaky and shouldn&#39;t affect other methods in the same service.</p>
<h3>2. Idempotency and the &quot;Single Shot&quot;</h3>
<p>In gRPC, calls are considered repeatable by default. While this is usually true for reads, it is dangerous for mutations (like <code>ChargeCard</code>).</p>
<p>You can define which methods are repeatable in the options, or use <code>GrpcResilience.SingleShot()</code> for call-site overrides. This creates an ambient scope that disables retries for any call made within that block, regardless of the policy.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// 1. Policy-level: mark a specific method as non-repeatable</span>
</span><span class="md-code-line"><span class="tok">services</span><span class="tok-punct">.</span><span class="tok-method">AddGrpcClient</span><span class="tok-punct">&lt;</span><span class="tok-type">Orders</span><span class="tok-punct">.</span><span class="tok-type">OrdersClient</span><span class="tok-punct">&gt;(</span><span class="tok">o </span><span class="tok-punct">=&gt;</span><span class="tok"> o</span><span class="tok-punct">.</span><span class="tok-type">Address</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">Uri</span><span class="tok-punct">(</span><span class="tok-string">&quot;https://orders.internal:5001&quot;</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">AddGrpcResilience</span><span class="tok-punct">(</span><span class="tok">options </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">        options</span><span class="tok-punct">.</span><span class="tok-type">RepeatableWhen</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">static</span><span class="tok"> m </span><span class="tok-punct">=&gt;</span><span class="tok"> m</span><span class="tok-punct">.</span><span class="tok-type">Name</span><span class="tok"> </span><span class="tok-punct">!=</span><span class="tok"> </span><span class="tok-string">&quot;ChargeCard&quot;</span><span class="tok-punct">;</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">});</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// 2. Call-site: force a single attempt</span>
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-type">GrpcResilience</span><span class="tok-punct">.</span><span class="tok-method">SingleShot</span><span class="tok-punct">())</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">await</span><span class="tok"> client</span><span class="tok-punct">.</span><span class="tok-method">ChargeCardAsync</span><span class="tok-punct">(</span><span class="tok">request</span><span class="tok-punct">);</span><span class="tok"> </span><span class="tok-comment">// Will not be retried</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><h3>3. The Math of Wire Deadlines</h3>
<p>One of the most critical parts of the gRPC integration is how NResilience handles <code>grpc-timeout</code>. </p>
<p>The library calculates the wire deadline as:
<code>min(AttemptTimeout, remaining overall deadline) + DeadlineSlack</code></p>
<p>The <code>DeadlineSlack</code> (defaulting to 50ms) is a deliberate design choice. By giving the wire deadline a small buffer, NResilience ensures that its own internal timer expires first. This allows the library to throw an <code>AttemptTimeoutException</code> and correctly track the attempt as a failure, rather than receiving a generic <code>StatusCode.DeadlineExceeded</code> from the gRPC stack. It ensures the library, not the transport, remains the authoritative source of truth.</p>
<h3>4. Handling Server Streams</h3>
<p>Retrying a stream is tricky. NResilience supports server streaming with a specific rule: <strong>retries are only attempted until the first message is received.</strong></p>
<p>Once the consumer has received the first element of the <code>IAsyncEnumerable</code>, the call is considered &quot;started.&quot; Any failure after that point is propagated directly to the consumer. This prevents the library from accidentally triggering a full stream restart, which would likely lead to duplicate data or missing messages. For a stream, the wire deadline is the whole call&#39;s remaining budget rather than one attempt&#39;s ceiling. Client-streaming and duplex calls pass through untouched - wrapping one means re-enumerating something the failed attempt already partially consumed, which is not a resilience feature.</p>
<h3>5. Why No Blocking Calls?</h3>
<p>You will notice that <code>BlockingUnaryCall</code> is not supported and will throw a <code>NotSupportedException</code>.</p>
<p>NResilience is built as an async-first library to maximize throughput and minimize allocations. A blocking call that silently ran without the policy would be the one call in the client with no retry, no breaker, and no deadline - a worse outcome than a visible failure at the one call site that uses it. The same reasoning is why a synchronous <code>Send</code> throws in the HTTP handler. Use the generated client&#39;s <code>Async</code> overloads.</p>
<h2>Trade-offs and Limitations</h2>
<p>The per-service scoping adds a small amount of overhead to the interceptor to resolve the service name for every call. While negligible for most, in extremely high-throughput environments, this is the cost of preventing a total client brownout.</p>
<p>The &quot;first message&quot; rule for streams means that if your stream is designed to be resumable (using offsets or tokens), NResilience&#39;s built-in retry won&#39;t help you after the first message. You would need to implement resumption logic at the application level.</p>
<h2>Conclusion</h2>
<p>gRPC resilience isn&#39;t about wrapping a call in a loop; it&#39;s about managing the contract between the interceptor and the wire. By scoping breakers to services and carefully timing the <code>grpc-timeout</code>, NResilience provides protection that respects the semantics of RPCs.</p>
<p>The <a href="https://docs.nresilience.net/grpc/deadlines">Deadlines documentation</a> details the wire format and <code>DeadlineSlack</code>. For the engine underneath both the HTTP and gRPC integrations, <a href="https://nresilience.net/blog/one-loop-one-allocation">One loop, one allocation</a> explains the flat executor and how it keeps a single async frame regardless of policy complexity.</p>
]]></content:encoded>
    </item>
    <item>
      <title>HTTP in depth</title>
      <link>https://nresilience.net/blog/http-in-depth</link>
      <guid isPermaLink="true">https://nresilience.net/blog/http-in-depth</guid>
      <pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate>
      <description>Learn how NResilience handles per-host state, transport timeouts, wire idempotency, and nested retry detection in .NET HTTP clients.</description>
      <category>Per stack</category>
      <category>http</category>
      <category>deadlines</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Resilience at the HTTP level requires more than just a retry loop. This post covers how NResilience manages independent breakers per host, resolves the conflict between HttpClient and library timeouts, ensures safe retries via idempotency keys, prevents retry storms across microservices using a marker header, and reads the rate limit the dependency publishes so you never have to guess it.</p>
<h2>The Problem: The &quot;One Client&quot; Fallacy</h2>
<p>When you build an <code>HttpClient</code> with resilience, it is easy to think of it as a single pipe. But in a real app, that client often calls multiple different hosts - a payment gateway, a user profile service, and a legacy SOAP API.</p>
<p>If you use a single global circuit breaker, a brownout at the payment gateway trips the breaker for everyone. Your user profile service is healthy, but your app stops calling it because the &quot;client&quot; is broken. This is a blast-radius problem. </p>
<p>Then there is the wire problem. Retrying a <code>GET</code> is safe. Retrying a <code>POST</code> that creates an order can be catastrophic if the server processed the first request but the network failed on the way back. You need a way to signal to the server that &quot;this is a retry of request X&quot; so it doesn&#39;t charge the customer twice.</p>
<p>Finally, in a microservice chain, if Service A retries Service B, and Service B retries Service C, a single failure at the bottom can trigger an exponential explosion of requests. This is a retry storm, and it can turn a minor glitch into a full-site outage.</p>
<h2>The Solution: HTTP-Aware Resilience</h2>
<p>NResilience doesn&#39;t treat HTTP as just another callback. It implements specific wire conventions and scoping rules to handle these distributed systems problems.</p>
<h3>1. Per-Host State</h3>
<p>To solve the blast-radius problem, NResilience supports per-host breakers and budgets. Instead of one breaker per client, you can have one breaker per unique host.</p>
<p>When you configure the client, you can set <code>MaximumHosts</code> to bound the memory used by the host registry. If you call a new host, the library creates a new breaker for it. If the registry hits the limit, it evicts the least recently used host.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// In practice you get the handler through HttpResilience.CreateClient() or AddResilience()</span>
</span><span class="md-code-line"><span class="tok-comment">// on a client builder - both of which accept HttpResilienceOptions.</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> options </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">HttpResilienceOptions</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">MaximumHosts</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">64</span><span class="tok"> </span><span class="tok-comment">// Track up to 64 unique hosts independently</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> client </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">HttpResilience</span><span class="tok-punct">.</span><span class="tok-method">CreateClient</span><span class="tok-punct">(</span><span class="tok">options</span><span class="tok-punct">:</span><span class="tok"> options</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// The breaker for api.payments.com can be Open</span>
</span><span class="md-code-line"><span class="tok-comment">// while the breaker for api.users.com remains Closed.</span>
</span></code></pre><p>This ensures that a failure in one dependency does not starve your other dependencies. You can inspect the state of these breakers via <code>handler.BreakersByHost()</code> to see exactly which host is struggling.</p>
<h3>2. Resolving the Timeout Conflict</h3>
<p>There is a classic conflict between <code>HttpClient.Timeout</code> and resilience deadlines. The transport timeout defaults to 100 seconds and covers the entire request sequence - including all attempts and backoff delays - so it silently caps any policy with a longer deadline, invisibly to the policy.</p>
<p>NResilience solves this with <code>OwnTransportTimeout</code>. When enabled (it is on by default), the library sets <code>HttpClient.Timeout</code> to infinite so the policy&#39;s deadline is the only active bound.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> options </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">HttpResilienceOptions</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">OwnTransportTimeout</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">true</span><span class="tok"> </span><span class="tok-comment">// HttpClient.Timeout becomes infinite (the default)</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p>This makes the NResilience deadline the single source of truth. Whether it is a 30s global deadline or a 2s attempt timeout, the library controls the clock, and the transport just executes the bytes.</p>
<p>Which raises the obvious question about the bytes. Taking ownership of the transport timeout is the right trade - two silent timeout systems are worse than one - but it removes the only bound that ever applied to the response <em>body</em>. An attempt ends when the headers arrive; the body is a live stream you read afterwards, so it is covered by neither the deadline nor the attempt timeout nor the retry, and now not by a 100-second backstop either. A server that sends headers and then stops writing used to give you a call that never returned.</p>
<p><code>BoundProgress</code> closes that, and it is on by default. The bound is the <code>AttemptTimeout</code> you already set, applied to the gap between two reads: a body that goes quiet for longer than that fails your read with an <code>AttemptStalledException</code> rather than hanging on it. Because it bounds the gap and not the total, a large download that keeps arriving is never cut off.</p>
<p>The stall is bounded, not retried - the retry loop was over before the body existed. <code>BufferResponses</code> is the other side of that trade:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> options </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">HttpResilienceOptions</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">BufferResponses</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">true</span><span class="tok"> </span><span class="tok-comment">// read the body inside the attempt, so a stalled body is retried</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p>The body is now read before the attempt is judged, so both clocks cover it and a broken body is one more transient failure. The cost is memory: the whole body is held before the call returns. Leave it off for a client that downloads large files. For a JSON API whose caller was going to buffer the body a moment later anyway, it is close to free.</p>
<h3>3. Idempotency on the Wire</h3>
<p>Retrying a <code>POST</code> or <code>PATCH</code> is usually a bad idea. NResilience defaults to not retrying these methods. However, if your server supports idempotency keys, you can safely mark a request as repeatable.</p>
<p>The <code>MarkRepeatable</code> extension method does two things in one call: it tells <em>this client</em> the request may be sent again, and it stamps an idempotency key header (defaulting to <code>Idempotency-Key</code>) so <em>the server</em> can deduplicate the operation. Both halves matter - the option without a key duplicates the order, and the key without the option is never used.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> request </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">HttpRequestMessage</span><span class="tok-punct">(</span><span class="tok-type">HttpMethod</span><span class="tok-punct">.</span><span class="tok-type">Post</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-string">&quot;https://api.example.com/orders&quot;</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Content</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">JsonContent</span><span class="tok-punct">.</span><span class="tok-method">Create</span><span class="tok-punct">(</span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">Item</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-string">&quot;Book&quot;</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-type">Amount</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">1</span><span class="tok"> </span><span class="tok-punct">})</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Mark as repeatable with a unique key</span>
</span><span class="md-code-line"><span class="tok">request</span><span class="tok-punct">.</span><span class="tok-method">MarkRepeatable</span><span class="tok-punct">(</span><span class="tok">idempotencyKey</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-string">&quot;order-123-abc&quot;</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// The client now allows retries for this POST request</span>
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> response </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> client</span><span class="tok-punct">.</span><span class="tok-method">SendAsync</span><span class="tok-punct">(</span><span class="tok">request</span><span class="tok-punct">,</span><span class="tok"> cancellationToken</span><span class="tok-punct">);</span>
</span></code></pre><p>If the first attempt succeeds on the server but fails on the wire, the second attempt sends the same key. The server sees the key, recognizes the duplicate, and returns the original success response without processing the order again.</p>
<h3>4. Preventing Retry Storms</h3>
<p>In a chain of services, retries can multiply. If Service A retries 3 times, and it calls Service B which also retries 3 times, a single failure at the bottom results in 9 requests.</p>
<p>NResilience uses a marker header to make this visible. A retrying handler adds <code>X-NResilience-Retrying: 1</code> to every request it can retry - on the first attempt, not just on retries, because by the time a retry goes out the amplification has already happened.</p>
<p>On the server side, you add the <code>UseResilienceNestedRetry</code> middleware. It reads the marker and publishes the flag for the rest of the request, so any outbound calls that request makes report a <code>NestedRetry</code> event in telemetry: the middle hop of the chain can now see an amplification it is part of. The library reports but does not intervene - what you do with the signal (typically fewer attempts and a shorter deadline on the inner layer) is your call.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// In your ASP.NET Core Program.cs</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> builder </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">WebApplication</span><span class="tok-punct">.</span><span class="tok-method">CreateBuilder</span><span class="tok-punct">(</span><span class="tok">args</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> app </span><span class="tok-punct">=</span><span class="tok"> builder</span><span class="tok-punct">.</span><span class="tok-method">Build</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Detect incoming retries and report them for this request's outbound calls</span>
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">UseResilienceNestedRetry</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">MapGet</span><span class="tok-punct">(</span><span class="tok-string">&quot;/proxy&quot;</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-keyword">async</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-type">HttpClient</span><span class="tok"> client</span><span class="tok-punct">)</span><span class="tok"> </span><span class="tok-punct">=&gt;</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// If the caller sent X-NResilience-Retrying, every outbound call this</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// request makes reports a NestedRetry event in telemetry.</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">return</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> client</span><span class="tok-punct">.</span><span class="tok-method">GetStringAsync</span><span class="tok-punct">(</span><span class="tok-string">&quot;https://api.example.com/data&quot;</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-punct">});</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">Run</span><span class="tok-punct">();</span>
</span></code></pre><p>Used together with a lower attempt count on the inner layer, this turns an exponential growth of requests into a linear one, protecting your downstream services from being crushed by their own resilience policies.</p>
<h3>5. How Failures are Classified</h3>
<p>The library uses <code>Classifier.Http</code> to decide if a response is a failure that should trigger a retry or trip a breaker. Not every non-200 response is a failure.</p>
<ul>
<li><strong>Transient (<code>Verdict.Transient</code>):</strong> 500 Internal Server Error, 408 Request Timeout, or <code>HttpRequestException</code>. These are retried.</li>
<li><strong>Throttled (<code>Verdict.Throttled</code>):</strong> 429 Too Many Requests or 503 Service Unavailable. These are retried, but the library respects the <code>Retry-After</code> header to avoid hammering a throttled server.</li>
<li><strong>Ok (<code>Verdict.Ok</code>):</strong> 404 Not Found and the other 4xx statuses. These are answers, not failures; retrying will not change the outcome.</li>
</ul>
<p>This classification ensures that your breakers only trip on actual infrastructure failure, not on user errors.</p>
<h3>6. The Quota the Dependency Publishes</h3>
<p><code>Retry-After</code> is the dependency telling you what to do <em>after</em> it has already refused you. Most large APIs also tell you before: how much quota you have, how much is left, and when the window resets - on every response, not just the 429. GitHub, Stripe, Shopify and Atlassian all send it. Nothing else in .NET reads it.</p>
<p>The handler does, and it is on by default. It parses two shapes - the <a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/">structured fields</a> <code>RateLimit-Policy</code> and <code>RateLimit</code>, and the older <code>X-RateLimit-*</code> triple those fields exist to replace - keeps the numbers per host beside that host&#39;s breaker and budget, and refuses an attempt locally once what is left is inside the reserve:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> options </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">HttpResilienceOptions</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Quota</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Quota</span><span class="tok-punct">.</span><span class="tok-method">Reserving</span><span class="tok-punct">(</span><span class="tok">reserve</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">0.1</span><span class="tok-punct">)</span><span class="tok"> </span><span class="tok-comment">// hold a tenth of the published allowance back. The default.</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p><code>Reserve</code> is the only number in the feature, and it is a fraction rather than a rate, because the rate is the dependency&#39;s to publish. That is the whole difference between this and a limiter: <code>PermitsPerSecond</code> is a number somebody had to guess and then keep in sync with a quota they do not control.</p>
<p>The refusal is a local one, so it gets what every local refusal gets: the long backoff curve, the reset time honored as the pushback, no evidence against the host&#39;s breaker, and no charge against the retry budget - because the call never left the process. A <code>RejectedByQuota</code> event carries the time until the window resets, and the exception the caller sees is <code>RateLimitedException</code>.</p>
<p>A host that publishes nothing has no quota and the feature is invisible. A host that publishes a remaining count with no quota to take a fraction of has no denominator for the reserve, so it is refused only at zero. A malformed field is ignored rather than thrown - a header the library cannot parse is not a reason to fail a call that worked.</p>
<p>There is one failure mode worth stating outright. A server publishing a per-account quota while you are one of fifty pods will make every pod believe it owns the whole allowance. The reserve does not fix that; only the 429 does, and the 429 still works exactly as it always has. The honest use is a single-instance client, or a generous reserve - and <code>Quota = null</code> reads no headers at all.</p>
<h2>Trade-offs and Limitations</h2>
<p>Per-host scoping adds a small amount of memory overhead to track the host registry. While <code>MaximumHosts</code> prevents unbounded growth, in an environment where you call thousands of unique hosts, you may need to tune this value to avoid frequent evictions.</p>
<p>Nested retry detection requires the marker header to be passed through. If you have a non-NResilience proxy or a legacy service in the middle of your chain that strips unknown headers, the suppression signal is lost.</p>
<p>The published quota is read per process, not per fleet. It can slow you down and cannot break you - it only ever refuses an attempt, never permits one the policy would have refused - but a shared per-account quota read by several instances is a number each of them will over-count. Lower the reserve, or turn it off, when that is your shape.</p>
<h2>Conclusion</h2>
<p>Resilience in HTTP is about the contract between the client and the wire. By moving from global state to per-host state, resolving timeout conflicts, and using marker headers for idempotency and nested retries, you can build a system that fails gracefully without amplifying the outage.</p>
<p>The <a href="https://docs.nresilience.net/http">HTTP documentation</a> covers the wire formats and middleware in detail. For how the same rules translate to the RPC world, <a href="https://nresilience.net/blog/grpc-in-depth">gRPC in depth</a> explains per-service scoping, the <code>grpc-timeout</code> math, and why blocking calls are not supported.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Tests that don&apos;t flake</title>
      <link>https://nresilience.net/blog/tests-that-dont-flake</link>
      <guid isPermaLink="true">https://nresilience.net/blog/tests-that-dont-flake</guid>
      <pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate>
      <description>How to write deterministic resilience tests using scripted callbacks, fake clocks, and event recording.</description>
      <category>Making sure it works</category>
      <category>testing</category>
      <category>determinism</category>
      <category>simulation</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Resilience is hard to test because it depends on timers and flakiness. NResilience solves this by replacing real time with <code>FakeTimeProvider</code> and real dependencies with <code>Sequence&lt;T&gt;</code>, turning non-deterministic race conditions into predictable, scripted tests.</p>
<h2>The &quot;Sleep&quot; Trap</h2>
<p>Most resilience tests suffer from the same pattern: they use <code>Task.Delay</code> or <code>Thread.Sleep</code> to simulate a dependency outage, then <code>await</code> for a few seconds and hope the retry logic fired.</p>
<p>This is the primary source of &quot;flaky tests.&quot; On a loaded CI server, a 100ms sleep might actually take 200ms, causing a timeout test to fail randomly. Worse, testing a 30-second circuit breaker break duration means your test suite takes 30 seconds to run.</p>
<p>Resilience should be tested as a state machine, not a stopwatch.</p>
<h2>Scripting the Dependency</h2>
<p>Instead of mocking an interface and trying to maintain an internal counter of how many times a method was called, NResilience provides <code>Sequence&lt;T&gt;</code>.</p>
<p>A sequence is a pre-defined script of outcomes. You define exactly what the dependency does on the first call, the second call, and so on.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">.</span><span class="tok-type">Testing</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// 1. Script the dependency: fail twice, then succeed</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> calls </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Sequence</span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Throws</span><span class="tok-punct">(</span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">HttpRequestException</span><span class="tok-punct">(</span><span class="tok-string">&quot;Transient error&quot;</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Throws</span><span class="tok-punct">(</span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">HttpRequestException</span><span class="tok-punct">(</span><span class="tok-string">&quot;Transient error&quot;</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Returns</span><span class="tok-punct">(</span><span class="tok-string">&quot;Success!&quot;</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// 2. Run the policy against the script</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> result </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> calls</span><span class="tok-punct">.</span><span class="tok-method">NextAsync</span><span class="tok-punct">(</span><span class="tok">attempt</span><span class="tok-punct">),</span>
</span><span class="md-code-line"><span class="tok">    cancellationToken</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">Equal</span><span class="tok-punct">(</span><span class="tok-string">&quot;Success!&quot;</span><span class="tok-punct">,</span><span class="tok"> result</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">Equal</span><span class="tok-punct">(</span><span class="tok-number">3</span><span class="tok-punct">,</span><span class="tok"> calls</span><span class="tok-punct">.</span><span class="tok-type">CallCount</span><span class="tok-punct">);</span>
</span></code></pre><p>Because <code>Sequence&lt;T&gt;</code> is a simple list of outcomes, there is no ambiguity about what the dependency returned or when it threw.</p>
<h2>Controlling Time</h2>
<p>To make these scripts deterministic, you must control the clock. <code>NResilience</code> policies accept a <code>TimeProvider</code>. In tests, you use <code>FakeTimeProvider</code>.</p>
<p>When a policy decides to sleep for a backoff delay, it doesn&#39;t block the thread - it asks the <code>TimeProvider</code> to notify it when the time has passed. By using a fake clock, you can &quot;warp&quot; time forward instantaneously.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> time </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">FakeTimeProvider</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">TestPolicy</span><span class="tok-punct">.</span><span class="tok-method">WithClock</span><span class="tok-punct">(</span><span class="tok">time</span><span class="tok-punct">);</span><span class="tok"> </span><span class="tok-comment">// Instant, on the test clock</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> calls </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Sequence</span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;(</span><span class="tok">time</span><span class="tok-punct">:</span><span class="tok"> time</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Delays</span><span class="tok-punct">(</span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">10</span><span class="tok-punct">))</span><span class="tok"> </span><span class="tok-comment">// Simulate a slow call</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Returns</span><span class="tok-punct">(</span><span class="tok-string">&quot;Done&quot;</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> task </span><span class="tok-punct">=</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> calls</span><span class="tok-punct">.</span><span class="tok-method">NextAsync</span><span class="tok-punct">(</span><span class="tok">attempt</span><span class="tok-punct">));</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// The task is currently suspended on the 10s delay.</span>
</span><span class="md-code-line"><span class="tok-comment">// Warp time forward to trigger the completion.</span>
</span><span class="md-code-line"><span class="tok">time</span><span class="tok-punct">.</span><span class="tok-method">Advance</span><span class="tok-punct">(</span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">10</span><span class="tok-punct">));</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> result </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> task</span><span class="tok-punct">;</span>
</span></code></pre><p>This turns a 10-second test into a 10-millisecond test.</p>
<h2>Asserting on Intent, Not Outcome</h2>
<p>Asserting that a method returned &quot;Success&quot; is not enough. You need to know <em>how</em> it got there. Did it succeed on the first try? Did it retry twice? Did the circuit breaker trip?</p>
<p>The <code>EventRecorder</code> is a specialized listener that captures every <code>CallEvent</code> raised by the policy. It allows you to assert on the exact sequence of resilience events.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> events </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">EventRecorder</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">TestPolicy</span><span class="tok-punct">.</span><span class="tok-type">InstantHttp</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">OnEvent</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> events</span><span class="tok-punct">.</span><span class="tok-type">Record</span><span class="tok"> </span><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> calls </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Sequence</span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Throws</span><span class="tok-punct">(</span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">HttpRequestException</span><span class="tok-punct">(</span><span class="tok-string">&quot;Transient&quot;</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Returns</span><span class="tok-punct">(</span><span class="tok-string">&quot;OK&quot;</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> calls</span><span class="tok-punct">.</span><span class="tok-method">NextAsync</span><span class="tok-punct">(</span><span class="tok">attempt</span><span class="tok-punct">));</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Assert on the exact order of events</span>
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">Equal</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-attr">[CallEventKind</span><span class="tok-punct">.</span><span class="tok-type">Attempt</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-type">CallEventKind</span><span class="tok-punct">.</span><span class="tok-type">Retrying</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-type">CallEventKind</span><span class="tok-punct">.</span><span class="tok-type">Attempt</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-type">CallEventKind</span><span class="tok-punct">.</span><span class="tok-type">Succeeded</span><span class="tok-punct">],</span>
</span><span class="md-code-line"><span class="tok">    events</span><span class="tok-punct">.</span><span class="tok-type">Kinds</span>
</span><span class="md-code-line"><span class="tok-punct">);</span>
</span></code></pre><p>By asserting on <code>events.Kinds</code>, you verify the internal logic of the policy (e.g., &quot;did it actually decide to retry?&quot;) rather than just the final result.</p>
<h2>Testing the HTTP Layer</h2>
<p>For those using the <code>HttpClient</code> integration, <code>ScriptedHttpHandler</code> provides the same deterministic scripting but at the <code>HttpMessageHandler</code> level. This allows you to test how your <code>HttpClient</code> handles different status codes without running a local web server.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> transport </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">ScriptedHttpHandler</span><span class="tok-punct">()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Responds</span><span class="tok-punct">(</span><span class="tok-type">HttpStatusCode</span><span class="tok-punct">.</span><span class="tok-type">ServiceUnavailable</span><span class="tok-punct">,</span><span class="tok"> times</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">2</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Responds</span><span class="tok-punct">(</span><span class="tok-type">HttpStatusCode</span><span class="tok-punct">.</span><span class="tok-type">OK</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> client </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">HttpResilience</span><span class="tok-punct">.</span><span class="tok-method">CreateClient</span><span class="tok-punct">(</span><span class="tok">policy</span><span class="tok-punct">,</span><span class="tok"> innerHandler</span><span class="tok-punct">:</span><span class="tok"> transport</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> response </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> client</span><span class="tok-punct">.</span><span class="tok-method">GetAsync</span><span class="tok-punct">(</span><span class="tok">uri</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">Equal</span><span class="tok-punct">(</span><span class="tok-type">HttpStatusCode</span><span class="tok-punct">.</span><span class="tok-type">OK</span><span class="tok-punct">,</span><span class="tok"> response</span><span class="tok-punct">.</span><span class="tok-type">StatusCode</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">Equal</span><span class="tok-punct">(</span><span class="tok-number">3</span><span class="tok-punct">,</span><span class="tok"> transport</span><span class="tok-punct">.</span><span class="tok-type">CallCount</span><span class="tok-punct">);</span>
</span></code></pre><h2>Measuring the Whole Configuration</h2>
<p>Scripts and fake clocks prove what one call does. They say nothing about what a hundred and fifty thousand of them cost the dependency, and that is the question a reviewer actually asks.</p>
<p><code>Simulate</code> answers it. It runs your real policy - the real executor, breaker, retry budget, classifier and estimators - against a modeled dependency on a virtual clock, and reports what the run cost:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> report </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Simulate</span><span class="tok-punct">.</span><span class="tok-method">Policy</span><span class="tok-punct">(</span><span class="tok">api</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Against</span><span class="tok-punct">(</span><span class="tok-type">Dependency</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">.</span><span class="tok-method">Healthy</span><span class="tok-punct">(</span><span class="tok">p50</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMilliseconds</span><span class="tok-punct">(</span><span class="tok-number">20</span><span class="tok-punct">),</span><span class="tok"> p99</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMilliseconds</span><span class="tok-punct">(</span><span class="tok-number">200</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">.</span><span class="tok-method">Brownout</span><span class="tok-punct">(</span><span class="tok">after</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">30</span><span class="tok-punct">),</span><span class="tok"> slower</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">8</span><span class="tok-punct">,</span><span class="tok"> lasting</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMinutes</span><span class="tok-punct">(</span><span class="tok-number">1</span><span class="tok-punct">)))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Under</span><span class="tok-punct">(</span><span class="tok-type">Load</span><span class="tok-punct">.</span><span class="tok-method">Constant</span><span class="tok-punct">(</span><span class="tok">perSecond</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">500</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">(</span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMinutes</span><span class="tok-punct">(</span><span class="tok-number">5</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">Run</span><span class="tok-punct">(</span><span class="tok">seed</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">42</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-type">Assert</span><span class="tok-punct">.</span><span class="tok-method">True</span><span class="tok-punct">(</span><span class="tok">report</span><span class="tok-punct">.</span><span class="tok-method">LoadMultiplier</span><span class="tok"> </span><span class="tok-punct">&lt;=</span><span class="tok"> </span><span class="tok-number">1.2</span><span class="tok-punct">);</span>
</span></code></pre><p>Five simulated minutes take about two seconds of wall clock, because nothing sleeps. <code>LoadMultiplier</code> is attempts that reached the dependency per call you made; <code>Amplification</code> is the same ratio in the worst single second, which is the one a dependency feels. <code>Availability</code>, <code>Latency(quantile)</code>, <code>BreakerOpens</code> and <code>TimeToRecover</code> round out the report.</p>
<p>Only three things are fake: the clock, the random source and the dependency. Nothing about the decision logic is re-implemented, because a simulator that modeled the library would be worse than no simulator. And the report reproduces to the last byte from a seed - on any operating system and any processor - so <code>Assert.True(report.LoadMultiplier &lt;= 1.2)</code> is a gate rather than an anecdote.</p>
<h2>Conclusion</h2>
<p>Flaky tests are the result of trying to test asynchronous time with real-world clocks. By combining <code>Sequence&lt;T&gt;</code> for behavior, <code>FakeTimeProvider</code> for time, and <code>EventRecorder</code> for intent, you can write resilience tests that are deterministic and execute in milliseconds. One rule to remember: pass the same <code>TimeProvider</code> to the policy and the script, or a scripted delay becomes a real sleep again.</p>
<p>Stop sleeping in your tests. Script your failures instead.</p>
<p>The <a href="https://docs.nresilience.net/testing">Testing documentation</a> covers the full <code>NResilience.Testing</code> package, and <a href="https://docs.nresilience.net/testing/simulation">Simulation</a> covers the report in detail. For injecting realistic, messy failures into your resilience pipeline, <a href="https://nresilience.net/blog/break-things-on-purpose">Break things on purpose</a> introduces the <code>Chaos</code> engine and how to run game days in production.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deadlines that travel</title>
      <link>https://nresilience.net/blog/deadlines-that-travel</link>
      <guid isPermaLink="true">https://nresilience.net/blog/deadlines-that-travel</guid>
      <pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate>
      <description>How to propagate deadlines and criticality across services in ASP.NET Core, avoiding the nested-retry storm and the &quot;zombie request&quot; problem.</description>
      <category>Running it for real</category>
      <category>deadlines</category>
      <category>criticality</category>
      <category>aspnetcore</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> A deadline is only as good as its propagation, and it is not the only thing that has to travel. NResilience for ASP.NET Core provides middleware to read inbound deadlines, criticality levels and retry markers, ensuring your outbound calls inherit the remaining time, know whether anyone is waiting, and avoid multiplying load on a struggling dependency.</p>
<h2>The &quot;Zombie Request&quot; Problem</h2>
<p>Imagine Service A calls Service B, and Service B calls Service C. </p>
<p>Service A has a 10-second deadline. It calls Service B. Service B takes 9 seconds to respond. Service A&#39;s deadline expires, and it hangs up. But Service B is still working - it has already sent a request to Service C, and it&#39;s waiting for an answer. </p>
<p>Service B is now processing a &quot;zombie request&quot;: the caller has already given up, but Service B is still consuming threads and memory to finish a job that no one is listening to. In a cascading failure, these zombies pile up, consuming all available resources and turning a partial outage into a total collapse.</p>
<p>To fix this, the deadline must travel.</p>
<h2>Propagating the Clock</h2>
<p>The solution is to pass the deadline as a header. When Service A calls Service B, it tells it how long this side is prepared to wait - each attempt carries <code>min(AttemptTimeout, time left on the deadline)</code> in whole milliseconds. Service B reads it and knows exactly how much of its own budget it can spend before the answer stops mattering.</p>
<p>NResilience makes this automatic in ASP.NET Core with <code>UseResilienceDeadline()</code>.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> app </span><span class="tok-punct">=</span><span class="tok"> builder</span><span class="tok-punct">.</span><span class="tok-method">Build</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Register before anything that makes an outbound call</span>
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">UseResilienceDeadline</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">MapGet</span><span class="tok-punct">(</span><span class="tok-string">&quot;/&quot;</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-punct">()</span><span class="tok"> </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-string">&quot;Hello World&quot;</span><span class="tok-punct">);</span>
</span></code></pre><p>This middleware reads the inbound deadline header and publishes it for the rest of the request. Any policy in your app that has <code>UseAmbientDeadline = true</code> is bounded by the tighter of two values: its own configured deadline, or the time the caller is actually still waiting.</p>
<h2>Propagating What Matters</h2>
<p>A deadline is not the only fact that stops at the process edge. How much a request matters stops there too, and the consequence is sharper than a zombie: Service C sheds the checkout and serves the nightly backfill, because from where it stands the two are identical.</p>
<p><code>AmbientCriticality</code> carries a four-level label the same way the deadline carries a number - <code>Sheddable</code>, <code>SheddablePlus</code>, <code>Critical</code>, <code>CriticalPlus</code>. The same <code>UseResilienceDeadline()</code> pass reads it, because both values arrive in the same headers collection and a second middleware would be a second walk over it for one lookup.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// The outbound half, on the HTTP options.</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> options </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-type">HttpResilienceOptions</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">PropagateCriticality</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">true</span><span class="tok"> </span><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// The inbound half, on the policy.</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> api </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">UseAmbientCriticality</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">true</span><span class="tok"> </span><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// And the label itself, for work that is not an inbound request.</span>
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> scope </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">AmbientCriticality</span><span class="tok-punct">.</span><span class="tok-method">Begin</span><span class="tok-punct">(</span><span class="tok-type">Criticality</span><span class="tok-punct">.</span><span class="tok-type">Sheddable</span><span class="tok-punct">);</span>
</span></code></pre><p>What the library does with a level is deliberately narrow. It does not shed, it does not reorder, and it does not decide what <code>Sheddable</code> should cost your service - that depends on which endpoints are cheap and which have a fallback, and a library cannot see either. What it does is the half your own code cannot reach, because it happens inside the executor:</p>
<ul>
<li>A <code>Sheddable</code> call is <strong>never hedged</strong>. Hedging spends capacity to buy latency, and there is no latency worth buying when nobody is waiting for the answer.</li>
<li>A <code>Sheddable</code> retry has to <strong>leave half the retry budget behind</strong>. While the dependency is healthy the bucket is full and nothing is refused; once it starts draining, the backfill stops and what remains is there for the checkout.</li>
</ul>
<p>Two guardrails are worth stating outright. An unlabeled call is <code>Critical</code>, never <code>Sheddable</code> - a default that sheds is a default that loses requests during the first incident after an upgrade. And <code>CriticalPlus</code> cannot arrive from the wire: a caller that could escalate itself would, and a level everyone claims is a level that means nothing. Inbound values clamp at <code>Critical</code>.</p>
<p>Shedding itself stays with <code>Admit</code>, which reads <code>AmbientCriticality.Current</code> like any other ambient fact. The point of the feature is that <code>Admit</code> has a value to read rather than a guess to make.</p>
<h2>Detecting the Retry Storm</h2>
<p>While deadlines prevent zombies, <strong>nested retries</strong> create storms.</p>
<p>If Service A retries a call to Service B three times, and Service B retries its call to Service C three times, a single failure at Service C can trigger 9 requests (3x3). If you have a chain of four services, you&#39;re looking at 81 requests for one logical operation. This is how a minor glitch becomes a self-inflicted DDoS attack.</p>
<p><code>UseResilienceNestedRetry()</code> solves this by marking the request.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> app </span><span class="tok-punct">=</span><span class="tok"> builder</span><span class="tok-punct">.</span><span class="tok-method">Build</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">UseResilienceDeadline</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">UseResilienceNestedRetry</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">MapGet</span><span class="tok-punct">(</span><span class="tok-string">&quot;/&quot;</span><span class="tok-punct">,</span><span class="tok"> </span><span class="tok-punct">()</span><span class="tok"> </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-string">&quot;Hello World&quot;</span><span class="tok-punct">);</span>
</span></code></pre><p>When Service A makes a call, its handler adds a marker (e.g., <code>X-NResilience-Retrying: 1</code>) to the request - on the first attempt, not just on retries, because by the time a retry goes out the amplification has already happened. Service B reads this marker with <code>UseResilienceNestedRetry()</code>, which publishes the flag for the rest of the request. When Service B then makes its own outbound calls, its handler fires a <code>NestedRetry</code> event saying: &quot;this request is already inside a retrying client.&quot;</p>
<p>The library reports but does not intervene: silently dropping configured retries would be surprising and hard to debug. What you do with the signal is yours - typically reducing the inner attempt count and shortening the inner deadline, so the layer closest to the failing dependency does the retrying and the layers above it pass through. That collapses the exponential growth of retries back into a linear flow.</p>
<h2>Mapping Failures to the Wire</h2>
<p>When a deadline expires or a retry budget is exhausted, the library throws a specific exception. You don&#39;t want these to result in a generic <code>500 Internal Server Error</code>, which tells the caller nothing.</p>
<p><code>AddResilienceExceptionHandler()</code> maps these internal exceptions to the correct HTTP responses:</p>
<ul>
<li><strong>Deadline Exceeded</strong> -&gt; <code>504 Gateway Timeout</code></li>
<li><strong>Breaker/Budget/Limiter Refusal</strong> -&gt; <code>503 Service Unavailable</code> (with a <code>Retry-After</code> header)</li>
</ul>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// In Program.cs</span>
</span><span class="md-code-line"><span class="tok">builder</span><span class="tok-punct">.</span><span class="tok-type">Services</span><span class="tok-punct">.</span><span class="tok-method">AddResilienceExceptionHandler</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok">builder</span><span class="tok-punct">.</span><span class="tok-type">Services</span><span class="tok-punct">.</span><span class="tok-method">AddProblemDetails</span><span class="tok-punct">();</span><span class="tok"> </span><span class="tok-comment">// Required for the exception handler</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> app </span><span class="tok-punct">=</span><span class="tok"> builder</span><span class="tok-punct">.</span><span class="tok-method">Build</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok">app</span><span class="tok-punct">.</span><span class="tok-method">UseExceptionHandler</span><span class="tok-punct">();</span><span class="tok"> </span><span class="tok-comment">// The framework middleware that runs the handler</span>
</span></code></pre><p>This tells the upstream caller exactly why the request failed. A <code>503</code> tells them to back off; a <code>504</code> tells them the system is too slow. This feedback loop is what allows the entire fleet to stabilize.</p>
<h2>Conclusion</h2>
<p>Resilience isn&#39;t something you configure in a vacuum; it&#39;s a conversation between services. By propagating deadlines and criticality and detecting nested retries, you stop your services from working on dead requests, keep a backfill from spending the capacity a checkout needs, and prevent your retry logic from becoming a weapon. </p>
<p>With three lines of middleware and one service registration, you move from &quot;hoping for the best&quot; to a system that actively coordinates its failure modes.</p>
<p>One more option on that middleware is worth knowing about. <code>RejectExpired</code> refuses a request that arrives with less time left than the <code>Reserve</code> says answering costs, with a <code>504</code> and a problem document, rather than running a handler whose every outbound call will fail. It is off by default and stays off: the request may well be answerable from cache, and refusing it is a decision about your service rather than about resilience.</p>
<p>The <a href="https://docs.nresilience.net/http">HTTP Deadline documentation</a> details header configuration and nested-retry detection. For testing these propagation rules deterministically, <a href="https://nresilience.net/blog/tests-that-dont-flake">Tests that don&#39;t flake</a> shows how to script failures using <code>Sequence&lt;T&gt;</code> and <code>FakeTimeProvider</code>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Logs that don&apos;t flood</title>
      <link>https://nresilience.net/blog/logs-that-dont-flood</link>
      <guid isPermaLink="true">https://nresilience.net/blog/logs-that-dont-flood</guid>
      <pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
      <description>How NResilience handles logging: silent when healthy, informative when failing, and flood-controlled to prevent log-noise during incidents.</description>
      <category>Running it for real</category>
      <category>logging</category>
      <category>observability</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Logging should be an alarm, not a background hum. NResilience uses a profile-based system that stays silent during healthy operations and employs a repeat-window to prevent a single failing call from flooding your logs with identical retry records.</p>
<h2>The &quot;Log Noise&quot; Problem</h2>
<p>In most systems, logging is binary: it&#39;s either on or off. When you turn on <code>Debug</code> or <code>Information</code> logging for a resilience library, you often get a wall of text: &quot;Attempt 1 started&quot;, &quot;Attempt 1 failed&quot;, &quot;Retrying in 100ms&quot;, &quot;Attempt 2 started&quot;. </p>
<p>In a high-throughput system, this is catastrophic. A single dependency brownout can generate millions of nearly identical log lines in seconds, filling up your disks and making it impossible to find the actual root cause in your log aggregator.</p>
<p>NResilience takes the opposite approach: the library decides the level, and the operator decides the filter.</p>
<h2>Profiles: Silent by Default</h2>
<p>Instead of relying on generic log levels, NResilience uses <strong>profiles</strong>. This allows the library to encode the &quot;meaning&quot; of an event into its severity.</p>
<p>You can choose from three profiles:</p>
<ul>
<li><code>Off</code>: Attaches no listener, so suppressed calls cost nothing.</li>
<li><code>Default</code>: Healthy traffic is <code>Trace</code>, retried-then-successful calls are <code>Debug</code>, incidents are <code>Warning</code>.</li>
<li><code>Verbose</code>: Raises every traffic-proportional record to <code>Information</code> and leaves incident records where they are. Exists for sinks that enforce a minimum level.</li>
</ul>
<p>The key is that in the <code>Default</code> profile, a healthy process writes nothing above <code>Trace</code>. A <code>Warning</code> means a breaker opened, a budget was exhausted, a callback outlived its timeout, or a nested retry was detected - the library encodes the meaning of an event into its severity, so a healthy call (even one that retried) stays out of your <code>Information</code> feed.</p>
<p>Logging is on by default for policies registered in a container, and opt-in for policies you build yourself:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// A policy registered in a container logs for you. A policy in a static field does not.</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> payments </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">Name</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-string">&quot;payments&quot;</span><span class="tok"> </span><span class="tok-punct">}).</span><span class="tok-method">WithLogging</span><span class="tok-punct">(</span><span class="tok">logger</span><span class="tok-punct">:</span><span class="tok"> logger</span><span class="tok-punct">);</span>
</span></code></pre><h2>Flood Control: The Repeat Window</h2>
<p>Even when a call is failing, you don&#39;t need to see every single retry. If a call is retrying 10 times, and every attempt fails with the same <code>503 Service Unavailable</code>, 10 log lines are not 10x more useful than one.</p>
<p>NResilience implements <strong>flood control</strong> via a repeat-window (30 seconds by default, per policy and reason). When a guard refuses a call, the first refusal writes a <code>Warning</code>. Refusals within the window are counted and written as <code>Debug</code> records, and the count is included in the next warning: &quot;Rejections logged quietly since the previous warning: 1483.&quot;</p>
<p>No records are dropped, only demoted. You still see every rejection at <code>Debug</code> if your filter carries it - the window keeps the <code>Warning</code> stream legible during a total dependency collapse.</p>
<h2>Per-Policy Filtering</h2>
<p>The NResilience logging system handles categories well. Every policy logs under its own unique category: <code>NResilience.&lt;PolicyName&gt;</code>.</p>
<p>This means you can keep the entire library at <code>Warning</code> level, but turn on <code>Debug</code> logging for just one specific, problematic dependency in your <code>appsettings.json</code> without a redeploy:</p>
<pre class="md-code md-code-text"><code><span class="md-code-line"><span class="tok">{</span>
</span><span class="md-code-line"><span class="tok">  &quot;Logging&quot;: {</span>
</span><span class="md-code-line"><span class="tok">    &quot;LogLevel&quot;: {</span>
</span><span class="md-code-line"><span class="tok">      &quot;NResilience&quot;: &quot;Warning&quot;,</span>
</span><span class="md-code-line"><span class="tok">      &quot;NResilience.payments&quot;: &quot;Debug&quot;,</span>
</span><span class="md-code-line"><span class="tok">      &quot;NResilience.identity&quot;: &quot;Information&quot;</span>
</span><span class="md-code-line"><span class="tok">    }</span>
</span><span class="md-code-line"><span class="tok">  }</span>
</span><span class="md-code-line"><span class="tok">}</span>
</span></code></pre><p>This allows you to &quot;zoom in&quot; on a specific part of your distributed system while keeping the rest of the noise filtered out.</p>
<h2>Sampling for Scale</h2>
<p>For ultra-high-throughput services, even &quot;silent-when-healthy&quot; logging can be too much during an incident. NResilience supports <strong>opt-in log sampling</strong>.</p>
<p><code>LogSampling.OneIn(n)</code> keeps one traffic record in n while the policy is healthy, and every record for a minute after an incident - so you get a consistent stream of data when it matters, and a sampled rate the rest of the time. The counting is exact rather than random: every nth record, per record and per policy.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok">services</span><span class="tok-punct">.</span><span class="tok-method">AddResilienceLogging</span><span class="tok-punct">(</span><span class="tok">o </span><span class="tok-punct">=&gt;</span><span class="tok"> o</span><span class="tok-punct">.</span><span class="tok-type">Sampling</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">LogSampling</span><span class="tok-punct">.</span><span class="tok-method">OneIn</span><span class="tok-punct">(</span><span class="tok">keepOneIn</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">20</span><span class="tok-punct">));</span>
</span></code></pre><h2>Integration with <code>WithLogging()</code></h2>
<p>If you are using the <code>NResilience.Extensions</code> package, logging is often handled for you. When you register policies in a DI container, the library automatically attaches a log listener.</p>
<p>If you need to tune the profile or levels, use <code>WithLogging()</code> with a <code>ResilienceLoggingOptions</code> callback - it takes a logger first, and a level-override function or profile in the options.</p>
<p>An explicit <code>WithLogging</code> call wins over the automatic container-based listener, so your specific tuning takes precedence. At most one log listener attaches per policy, and the first one attached wins.</p>
<h2>Conclusion</h2>
<p>Logs are only useful if they are actionable. By shifting the responsibility of &quot;level&quot; to the library and providing tools like repeat-windows and per-policy categories, NResilience ensures that your logs are a signal, not noise. You get the information you need when things break, and total silence when they don&#39;t.</p>
<p>The <a href="https://docs.nresilience.net/features/logging">Logging documentation</a> covers configuring profiles and sampling. For what your logs look like when requests start crossing service boundaries, <a href="https://nresilience.net/blog/deadlines-that-travel">Deadlines that travel</a> covers deadline propagation and nested retry detection.</p>
]]></content:encoded>
    </item>
    <item>
      <title>One event per call</title>
      <link>https://nresilience.net/blog/one-event-per-call</link>
      <guid isPermaLink="true">https://nresilience.net/blog/one-event-per-call</guid>
      <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
      <description>How NResilience handles telemetry: one terminal event per operation, the retry fraction, and allocation-free observation.</description>
      <category>Running it for real</category>
      <category>telemetry</category>
      <category>observability</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Every resilient operation in NResilience ends with exactly one terminal <code>CallEvent</code>. By splitting logical calls from wire-level attempts, you get a single, reliable metric - the retry fraction - that tells you if you&#39;re serving errors or fueling a retry storm.</p>
<h2>The telemetry gap</h2>
<p>Most resilience libraries log from the perspective of the attempt. You see a &quot;Retry 1&quot; log, then a &quot;Retry 2&quot; log, and eventually a success or a failure. This is fine for debugging a single request, but it&#39;s useless for operating a fleet.</p>
<p>When you&#39;re looking at a dashboard during an incident, you don&#39;t care that a specific request retried twice. You care about the aggregate: is my system performing 1.1x the expected work, or 5x? If you only have &quot;per-attempt&quot; metrics, you have to guess the logical call volume by deduplicating trace IDs or relying on a separate high-level span that might be missing or sampled differently.</p>
<p>NResilience treats the logical operation as the primary unit of account. Every call, regardless of how many attempts it takes, raises exactly one terminal event.</p>
<h2>Anatomy of a <code>CallEvent</code></h2>
<p>The <code>CallEvent</code> is the entire telemetry surface of the library. Instead of a family of differently-shaped event types, everything - from a successful return to a circuit breaker refusal - is a <code>CallEvent</code>.</p>
<p>It is a <code>readonly struct</code> (64 bytes), passed by value to listeners. This means raising an event allocates nothing. If you don&#39;t have a listener attached, the executor suppresses the event logic entirely, so the cost is zero.</p>
<p>The most useful part of the event for operators is the <code>ToString()</code> output, which follows a strict, predictable format:
<code>[PolicyName] Kind #Attempt VerdictKind ExceptionType (duration) +delay</code></p>
<p>Here is what that looks like for a call that retried once and succeeded:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok">[Api] Attempt #1 Transient HttpRequestException (102ms)</span>
</span><span class="md-code-line"><span class="tok">[Api] Retrying #1 Transient (102ms) +200ms</span>
</span><span class="md-code-line"><span class="tok">[Api] Attempt #2 Ok (45ms)</span>
</span><span class="md-code-line"><span class="tok">[Api] Succeeded #2 Ok (347ms)</span>
</span></code></pre><p>In this sequence, you see two attempts, one retry, and one terminal event (<code>Succeeded</code>). The duration in the terminal event is the total wall-clock time the caller waited.</p>
<h2>Listening to the executor</h2>
<p>You can hook into these events using the <code>OnEvent</code> property on your policy.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> apiPolicy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">OnEvent</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> e </span><span class="tok-punct">=&gt;</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-keyword">if</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok">e</span><span class="tok-punct">.</span><span class="tok-type">IsTerminal</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">            </span><span class="tok-type">Console</span><span class="tok-punct">.</span><span class="tok-method">WriteLine</span><span class="tok-punct">(</span><span class="tok-string">$&quot;Call ended: </span><span class="tok-punct">{</span><span class="tok">e</span><span class="tok-punct">.</span><span class="tok-type">Kind</span><span class="tok-punct">}</span><span class="tok-string"> in </span><span class="tok-punct">{</span><span class="tok">e</span><span class="tok-punct">.</span><span class="tok-type">Duration</span><span class="tok-punct">.</span><span class="tok-type">TotalMilliseconds</span><span class="tok-punct">}</span><span class="tok-string">ms&quot;</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p>To add a listener rather than replace one, use <code>WithListener</code>:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> counted </span><span class="tok-punct">=</span><span class="tok"> apiPolicy</span><span class="tok-punct">.</span><span class="tok-method">WithListener</span><span class="tok-punct">(</span><span class="tok">e </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-type">Metrics</span><span class="tok-punct">.</span><span class="tok-method">Record</span><span class="tok-punct">(</span><span class="tok">e</span><span class="tok-punct">.</span><span class="tok-type">Kind</span><span class="tok-punct">));</span>
</span></code></pre><p>That distinction matters more than it looks. Assigning <code>OnEvent</code> in a <code>with</code> expression <em>replaces</em> whatever was there - and a policy registered in a container already has telemetry and logging attached, so a <code>with { OnEvent = mine }</code> silently turns both off. <code>WithListener</code> adds to the chain, which is exactly what <code>WithTelemetry()</code> and <code>WithLogging()</code> do to each other. Listeners run in the order they were added.</p>
<p>Because the event is a struct, you can leave these listeners attached in production without worrying about GC pressure. The library only populates the <code>Result</code> property (which boxes the return value) if a listener is actually present, ensuring that the &quot;happy path&quot; remains allocation-free.</p>
<h2>Automatic telemetry with <code>WithTelemetry()</code></h2>
<p>While custom listeners are great for logging, you usually want your metrics in Prometheus or Grafana. The <code>NResilience.Extensions</code> package provides <code>WithTelemetry()</code>, which attaches a pre-configured listener that pipes events into <code>System.Diagnostics.Metrics</code> and <code>System.Diagnostics.Activity</code>. A policy registered in a container is instrumented for you; a policy in a static field needs this call.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">.</span><span class="tok-type">Extensions</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">WithTelemetry</span><span class="tok-punct">();</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// In your DI setup</span>
</span><span class="md-code-line"><span class="tok">builder</span><span class="tok-punct">.</span><span class="tok-type">Services</span><span class="tok-punct">.</span><span class="tok-method">AddOpenTelemetry</span><span class="tok-punct">()</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">WithMetrics</span><span class="tok-punct">(</span><span class="tok">m </span><span class="tok-punct">=&gt;</span><span class="tok"> m</span><span class="tok-punct">.</span><span class="tok-method">AddMeter</span><span class="tok-punct">(</span><span class="tok-string">&quot;NResilience&quot;</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">.</span><span class="tok-method">WithTracing</span><span class="tok-punct">(</span><span class="tok">t </span><span class="tok-punct">=&gt;</span><span class="tok"> t</span><span class="tok-punct">.</span><span class="tok-method">AddSource</span><span class="tok-punct">(</span><span class="tok-string">&quot;NResilience&quot;</span><span class="tok-punct">));</span>
</span></code></pre><p>In an HTTP pipeline, <code>AddResilience()</code> on the client builder attaches telemetry as part of registration and starts a single logical span for the entire operation. All subsequent attempt events, retry delays, and breaker transitions are added as events to that one span. This gives you a clean, single-entry trace for every logical request.</p>
<h2>The most important number: The Retry Fraction</h2>
<p>The goal of this architecture is to make one specific number easy to calculate: the <strong>retry fraction</strong>.</p>
<p>In your metrics provider, you&#39;ll see two primary counters:</p>
<ul>
<li><code>nresilience.calls</code>: Logical operations (one per call)</li>
<li><code>nresilience.attempts</code>: Wire-level attempts</li>
</ul>
<p>The formula is simple: <code>nresilience.attempts / nresilience.calls</code>.</p>
<p>If this number is <code>1.0</code>, you have no retries. If it&#39;s <code>1.1</code>, you&#39;re paying a 10% tax in extra load to hide transient failures - usually an acceptable trade. If it jumps to <code>3.0</code> or <code>5.0</code>, you are in a retry storm. Your clients are amplifying the load on a struggling dependency, likely making the outage worse.</p>
<p>By splitting these counters, NResilience turns a &quot;feeling&quot; about system load into a hard, observable metric.</p>
<h2>Monitoring the Tail</h2>
<p>Telemetry isn&#39;t just about counting failures; it&#39;s about seeing the system adapt. NResilience exposes its internal measurements through the same event system.</p>
<p>If you use hedging, you&#39;ll see <code>nresilience.hedges</code> and <code>nresilience.hedge.threshold</code>. The threshold is a histogram of the latency quantile that triggered the hedge. </p>
<p>Watching this move during an incident is how you tell the difference between a <strong>brownout</strong> and a <strong>tail</strong>:</p>
<ul>
<li><strong>Tail:</strong> The threshold stays stable, but a few requests are very slow. Hedges win often.</li>
<li><strong>Brownout:</strong> The threshold starts climbing. The dependency is slowing down for everyone. Hedges start failing or taking as long as the original call.</li>
</ul>
<p>Similarly, if you use measured bounds, the <code>nresilience.attempt.ceiling</code> and <code>nresilience.backoff.base</code> histograms tell you exactly when the library tightened a constant: each is recorded whenever the measured value moves, so a gap between the histogram and the constant you wrote is a measurement telling you your guess was off.</p>
<p>Four newer kinds round out the surface, each following the same one-terminal-event rule:</p>
<ul>
<li><code>SaturationDetected</code> - raised once at the onset of a thread-pool queue episode, so a count of these is a count of local incidents, not a per-call rate.</li>
<li><code>RejectedByQuota</code> - the HTTP handler refused one attempt because the dependency&#39;s published allowance is spent. It refuses an attempt rather than the call, so it is neither terminal nor an <code>IsRejection</code>.</li>
<li><code>StreamResumed</code> - a checkpointed stream or stalled HTTP body picked itself back up; <code>AttemptNumber</code> is the restart.</li>
<li><code>Draining</code> - the process is shutting down and the call stopped with the failure it already had instead of retrying. It is terminal, and it is a dependency failure, not a library refusal.</li>
</ul>
<h2>The cost of observation</h2>
<p>The core philosophy of NResilience is that you shouldn&#39;t pay for what you don&#39;t use. </p>
<p>If <code>OnEvent</code> is null, the executor performs a single null check before every event. It does not create <code>CallEvent</code> objects, it does not box results, and it does not allocate strings. </p>
<p>When you attach <code>WithTelemetry()</code>, the cost is a few bytes for the delegate and the overhead of the <code>System.Diagnostics</code> calls. Because we use <code>readonly structs</code> for the events, we avoid the common trap of allocating a new &quot;Event&quot; object for every single attempt in a high-throughput system.</p>
<h2>Conclusion</h2>
<p>Telemetry should be an asset, not a bottleneck. By centering the library around a single, allocation-free <code>CallEvent</code> and distinguishing between calls and attempts, NResilience gives you the data needed to run a fleet safely. You can see exactly what the executor is deciding, why it&#39;s deciding it, and exactly how much extra load your resilience is costing you.</p>
<p>The <a href="https://docs.nresilience.net/features/telemetry">Telemetry documentation</a> lists every instrument and tag. For the other half of the observability story, <a href="https://nresilience.net/blog/logs-that-dont-flood">Logs that don&#39;t flood</a> covers how the library keeps log noise under control during incidents.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Bending the library</title>
      <link>https://nresilience.net/blog/bending-the-library</link>
      <guid isPermaLink="true">https://nresilience.net/blog/bending-the-library</guid>
      <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
      <description>Explore the extension points of NResilience: custom admission, custom backoff, per-attempt hooks, and telemetry listeners.</description>
      <category>Power features</category>
      <category>extensibility</category>
      <category>hooks</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> NResilience uses a flat executor to keep performance high and allocations low. To avoid the complexity of &quot;strategy&quot; objects, it provides four targeted extension points - <code>Admit</code>, <code>BeforeAttempt</code>, <code>Backoff.Custom</code>, and <code>OnEvent</code> - that let you inject custom logic into the resilience loop without sacrificing the zero-allocation path.</p>
<h2>The extension surface</h2>
<p>Most resilience needs are covered by the built-in presets. You want a 30 s deadline, exponential backoff, and a circuit breaker - you use <code>Resilience.Http</code> and you&#39;re done. But real-world dependencies occasionally have requirements that don&#39;t fit a standard curve or a simple &quot;is this a 503?&quot; classifier.</p>
<p>Instead of opening up the executor to arbitrary &quot;strategy&quot; plugins - which would introduce virtual calls and layered overhead - NResilience exposes specific hooks. These are designed to be the minimum viable surface needed to handle 99% of custom cases while keeping the core loop flat.</p>
<p>The surface consists of:</p>
<ul>
<li><code>Admit</code>: A guard that decides if an attempt should even start.</li>
<li><code>BeforeAttempt</code>: A setup hook for preparing the call.</li>
<li><code>Backoff.Custom</code>: A way to calculate your own retry delays.</li>
<li><code>OnEvent</code>: A listener for every stage of a call, from attempts to the terminal outcome.</li>
</ul>
<h2>Admit: The classified guard</h2>
<p>The <code>Admit</code> hook is a <code>Func&lt;NextAttempt, Task&lt;Verdict&gt;&gt;</code>. It runs once per attempt, inside the same classified region as the call itself.</p>
<p>The key distinction here is that <code>Admit</code> returns a <code>Verdict</code>. If you return <code>Verdict.Limited</code> (or its alias <code>Verdict.Refused</code>), the executor treats the attempt as a self-imposed refusal: logged, exempt from the breaker and the retry budget, and reported in the terminal <code>CallEvent</code>. That exemption is deliberate - your own guard refusing a call is not evidence that the dependency is failing.</p>
<p>Use <code>Admit</code> when you have a guard whose outcome is naturally a decision rather than an exception - a distributed lock, a custom quota, anything that should refuse a call before it reaches the dependency. If throwing is more natural, the same classification works through a custom guard exception and a <code>Classifier</code> rule; both reach the identical code path.</p>
<p>There is a small performance trade-off: configuring <code>Admit</code> selects a separate execution path in the library to avoid paying an <code>await</code> tax on every single call. If you don&#39;t use it, you pay nothing. If you do, you pay one extra hoisted awaiter field in the state machine - a negligible cost for the flexibility it provides.</p>
<p>One thing to know if your guard answers from memory. The hook returns <code>Task&lt;Verdict&gt;</code>, not <code>ValueTask&lt;Verdict&gt;</code>, for the reason above - so a synchronous guard has to wrap its answer in a task on every attempt. Hand back <code>Verdict.OkTask</code> instead of <code>Task.FromResult(Verdict.Ok)</code>:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Admit</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> _ </span><span class="tok-punct">=&gt;</span><span class="tok"> gate</span><span class="tok-punct">.</span><span class="tok-method">Wait</span><span class="tok-punct">(</span><span class="tok-number">0</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">?</span><span class="tok"> </span><span class="tok-type">Verdict</span><span class="tok-punct">.</span><span class="tok-type">OkTask</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">Task</span><span class="tok-punct">.</span><span class="tok-method">FromResult</span><span class="tok-punct">(</span><span class="tok-type">Verdict</span><span class="tok-punct">.</span><span class="tok-method">Limited</span><span class="tok-punct">(</span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMilliseconds</span><span class="tok-punct">(</span><span class="tok-number">50</span><span class="tok-punct">))),</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p><code>Verdict.TransientTask</code> and <code>Verdict.PermanentTask</code> are the same thing for the other two constant verdicts. There is no cached task for <code>Throttled</code>, <code>Limited</code> or <code>Refused</code> - each takes a pushback, so there is no single value to cache, and a guard that refuses is on the slow path anyway, where the retry it causes costs far more than the task wrapping its answer.</p>
<p>The clearest case for it is priority-aware shedding, and the library deliberately stops one step short of doing it for you. <code>AmbientCriticality.Current</code> tells you how much the current call matters - a value that traveled with the request rather than one you guessed - and what <code>Sheddable</code> should cost your service is the part only you can answer:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Admit</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> _ </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-type">AmbientCriticality</span><span class="tok-punct">.</span><span class="tok-method">Current</span><span class="tok"> </span><span class="tok-punct">&lt;</span><span class="tok"> </span><span class="tok-type">Criticality</span><span class="tok-punct">.</span><span class="tok-type">Critical</span><span class="tok"> </span><span class="tok-punct">&amp;&amp;</span><span class="tok"> </span><span class="tok-method">Overloaded</span><span class="tok-punct">()</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">?</span><span class="tok"> </span><span class="tok-type">Task</span><span class="tok-punct">.</span><span class="tok-method">FromResult</span><span class="tok-punct">(</span><span class="tok-type">Verdict</span><span class="tok-punct">.</span><span class="tok-method">Refused</span><span class="tok-punct">(</span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">1</span><span class="tok-punct">)))</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">Verdict</span><span class="tok-punct">.</span><span class="tok-type">OkTask</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p>The library holds back the two things <code>Admit</code> cannot reach - it never hedges sheddable work, and it will not spend a depleted retry budget on it - and leaves shedding to the hook. See <a href="https://nresilience.net/blog/deadlines-that-travel">Deadlines that travel</a> for how the level gets there.</p>
<p><code>Admit</code> is one slot: setting it replaces whatever was there. That is deliberate. Combining two guards needs a rule for which refusal wins, and that is a decision about your system rather than one the library should make for you. Two guards are one hook that checks both.</p>
<h2>Backoff.Custom: Your own pacing</h2>
<p>Standard exponential backoff is the right answer for most things, but sometimes you need a specific pacing logic - perhaps based on the specific exception thrown or a custom window of time.</p>
<p><code>Backoff.Custom</code> takes a delegate that receives the <code>NextAttempt</code> context and returns a <code>TimeSpan</code>. This is where you implement your own pacing math.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Custom backoff that uses a linear increase for the first 3 attempts,</span>
</span><span class="md-code-line"><span class="tok-comment">// then switches to a hard constant delay.</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> customBackoff </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Backoff</span><span class="tok-punct">.</span><span class="tok-method">Custom</span><span class="tok-punct">(</span><span class="tok">attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">if</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok">attempt</span><span class="tok-punct">.</span><span class="tok-method">Number</span><span class="tok"> </span><span class="tok-punct">&lt;=</span><span class="tok"> </span><span class="tok-number">3</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-keyword">return</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromMilliseconds</span><span class="tok-punct">(</span><span class="tok-number">100</span><span class="tok"> </span><span class="tok-punct">*</span><span class="tok"> attempt</span><span class="tok-punct">.</span><span class="tok-type">Number</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok">    </span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">return</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">2</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-punct">});</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok-punct">{</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Backoff</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> customBackoff</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Attempts</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">5</span><span class="tok"> </span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p>One detail: server pushback always wins. If the previous verdict carried a <code>Retry-After</code> value, NResilience honors it verbatim, capping it by <code>MaximumDelay</code>. Your custom logic is only invoked when the server is silent about when you should return.</p>
<p>And a custom curve does not have to start from nothing. <code>Compute</code> is public, so a built-in curve can be the baseline and your delegate adjusts it - which is how you add a term to exponential backoff without reimplementing it:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> baseline </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Backoff</span><span class="tok-punct">.</span><span class="tok-method">Exponential</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> curve </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Backoff</span><span class="tok-punct">.</span><span class="tok-method">Custom</span><span class="tok-punct">(</span><span class="tok">next </span><span class="tok-punct">=&gt;</span><span class="tok"> baseline</span><span class="tok-punct">.</span><span class="tok-method">Compute</span><span class="tok-punct">(</span><span class="tok">next</span><span class="tok-punct">)</span><span class="tok"> </span><span class="tok-punct">+</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok">next</span><span class="tok-punct">.</span><span class="tok-type">Number</span><span class="tok"> </span><span class="tok-punct">-</span><span class="tok"> </span><span class="tok-number">1</span><span class="tok-punct">));</span>
</span></code></pre><h2>BeforeAttempt: The setup hook</h2>
<p><code>BeforeAttempt</code> is a <code>Func&lt;NextAttempt, Task&gt;</code>. Unlike <code>Admit</code>, it doesn&#39;t return a verdict - it just runs. It is the place to build a fresh request, refresh an auth token, or reset a stateful object before the attempt begins.</p>
<p>Crucially, <code>BeforeAttempt</code> runs outside the classification region. If it throws, the exception propagates out of the call immediately. It is not retried, not logged to the attempt log, and does not raise a <code>CallEvent</code>. This is intentional: if your setup fails, the resilience loop cannot recover it. Use it for setup that always has to run; if your guard needs retry, backoff, or telemetry, it belongs in the callback or in <code>Admit</code>.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-type">NResilience</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">BeforeAttempt</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> next </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-type">TokenProvider</span><span class="tok-punct">.</span><span class="tok-method">EnsureFreshTokenAsync</span><span class="tok-punct">(</span><span class="tok">next</span><span class="tok-punct">.</span><span class="tok-type">CancellationToken</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Usage: The executor runs BeforeAttempt, then the callback.</span>
</span><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    attempt </span><span class="tok-punct">=&gt;</span><span class="tok"> httpClient</span><span class="tok-punct">.</span><span class="tok-method">GetAsync</span><span class="tok-punct">(</span><span class="tok-string">&quot;https://api.example.com/data&quot;</span><span class="tok-punct">,</span><span class="tok"> attempt</span><span class="tok-punct">),</span>
</span><span class="md-code-line"><span class="tok">    cancellationToken</span><span class="tok-punct">);</span>
</span></code></pre><p>If you need a guard that can be retried or reported as a failure, use <code>Admit</code>. If you need to ensure a prerequisite is met before the clock starts ticking on the attempt, use <code>BeforeAttempt</code>.</p>
<h2>OnEvent: Telemetry without the tax</h2>
<p>NResilience follows a &quot;pay-for-play&quot; telemetry model. If you don&#39;t configure a listener, the executor raises nothing and pays nothing. No events are created, and no delegates are called.</p>
<p><code>OnEvent</code> is an <code>Action&lt;CallEvent&gt;</code>. It fires for every stage of a call: each attempt as it finishes, each retry decision, breaker transitions, and exactly one terminal event at the end. The terminal <code>CallEvent</code> contains the final verdict, the total number of attempts, and the cumulative delay. Every event is the same struct, so a listener can switch on <code>Kind</code> rather than subscribe to a family of differently-shaped types.</p>
<p>Because the listener is synchronous and runs on the executor&#39;s thread, you should never do I/O inside it. Log it, increment a counter, or enqueue it to a background channel. An exception thrown by the listener is swallowed; telemetry should never be able to crash the operation it is observing.</p>
<h2>The HTTP seam</h2>
<p>While the hooks above live on the <code>Resilience</code> policy, the best way to extend the library&#39;s behavior specifically for HTTP is through a <code>DelegatingHandler</code>.</p>
<p>By placing a handler in the <code>HttpClient</code> pipeline, you can modify requests or responses before they even hit the NResilience executor. This is the correct place for things like adding custom headers, tracing IDs, or implementing custom response-shaping that should happen before the classifier sees the result.</p>
<h2>The Hard No: Why no &quot;Strategy&quot; objects?</h2>
<p>You might wonder why NResilience doesn&#39;t allow you to implement a <code>IResilienceStrategy</code> interface and plug it into the loop. This is a deliberate design choice.</p>
<p>Layered strategy pipelines (the &quot;onion&quot; model) are flexible, but they are slow. Each layer adds a virtual call and a new frame to the state machine. In a high-throughput system, this adds up to significant CPU overhead and increased allocations.</p>
<p>NResilience replaces the pipeline with a flat loop. The executor knows exactly how to handle retries, deadlines, and breakers. By using targeted hooks like <code>Admit</code> and <code>BeforeAttempt</code>, you get the extension points you actually need without introducing the architectural bloat of a general-purpose strategy framework. </p>
<p>The library optimizes for the common case - the fast, allocation-free call - and provides a narrow, well-defined escape hatch for the edges.</p>
<h2>Conclusion</h2>
<p>Bending the library is about using the right tool for the right intent. Use <code>Admit</code> for guards that should trip the breaker, <code>BeforeAttempt</code> for required setup, <code>Backoff.Custom</code> for non-standard pacing, and <code>OnEvent</code> for lean telemetry. By keeping the extension surface small, NResilience keeps the execution path flat and the performance predictable.</p>
<p>The <code>OnEvent</code> listener is the key to understanding what the executor is doing. <a href="https://nresilience.net/blog/one-event-per-call">One event per call</a> explains the telemetry model and how the single terminal <code>CallEvent</code> gives you a clean view of your system&#39;s health.</p>
]]></content:encoded>
    </item>
    <item>
      <title>One policy per tenant</title>
      <link>https://nresilience.net/blog/one-policy-per-tenant</link>
      <guid isPermaLink="true">https://nresilience.net/blog/one-policy-per-tenant</guid>
      <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
      <description>Avoid the &quot;noisy neighbor&quot; problem. Learn how PolicyScope gives each tenant, shard, or channel its own breaker, budget, and latency estimate.</description>
      <category>Power features</category>
      <category>multi-tenancy</category>
      <category>policy-scope</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Shared breakers mean one noisy tenant can trip the circuit for everyone. <code>PolicyScope&lt;TKey&gt;</code> isolates resilience state per key, ensuring that a failure in one tenant&#39;s partition doesn&#39;t stop traffic for all other tenants.</p>
<h2>The Blast Radius Problem</h2>
<p>In a multi-tenant application, you often use a single shared dependency (like a database or a third-party API) to serve all your customers. </p>
<p>If you use a single <code>Breaker</code> for that dependency, you have a shared blast radius. If Tenant A sends a burst of malformed requests that trigger a series of failures, they will trip the breaker. Suddenly, Tenant B - who is behaving perfectly - finds their requests being refused because the breaker is open.</p>
<p>This is the &quot;noisy neighbor&quot; problem. Tenant A&#39;s failure has effectively caused an outage for Tenant B.</p>
<h2>Isolation via PolicyScope</h2>
<p>To fix this, you need to scope your resilience state. You don&#39;t want one breaker; you want one breaker <em>per tenant</em>.</p>
<p><code>PolicyScope&lt;TKey&gt;</code> allows you to define a template policy and then derive a specific instance of that policy for any given key.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// Define a template: a standard policy with a breaker</span>
</span><span class="md-code-line"><span class="tok-keyword">private</span><span class="tok"> </span><span class="tok-keyword">static</span><span class="tok"> </span><span class="tok-keyword">readonly</span><span class="tok"> </span><span class="tok-method">PolicyScope</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;</span><span class="tok"> </span><span class="tok-type">Tenants</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">Breaker</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok"> </span><span class="tok-method">Breaker</span><span class="tok-punct">()</span><span class="tok"> </span><span class="tok-punct">});</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// Use the policy for a specific tenant</span>
</span><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-type">Tenants</span><span class="tok-punct">.</span><span class="tok-method">For</span><span class="tok-punct">(</span><span class="tok">tenantId</span><span class="tok-punct">).</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">ct </span><span class="tok-punct">=&gt;</span><span class="tok"> </span><span class="tok-method">Work</span><span class="tok-punct">(</span><span class="tok">ct</span><span class="tok-punct">),</span><span class="tok"> cancellationToken</span><span class="tok-punct">);</span>
</span></code></pre><p>When you call <code>Tenants.For(tenantId)</code>, NResilience does something critical: it doesn&#39;t just give you the template. It creates a <strong>keyed instance</strong> of the policy. This instance has its own:</p>
<ul>
<li><strong>Circuit Breaker:</strong> Tenant A&#39;s failures only trip Tenant A&#39;s breaker.</li>
<li><strong>Retry Budget:</strong> Tenant A cannot exhaust the retry tokens for Tenant B.</li>
<li><strong>Latency Estimate:</strong> A slow partition for one tenant doesn&#39;t lower the hedge threshold for everyone else.</li>
</ul>
<h2>Guarding the Memory</h2>
<p>If you have millions of tenants, you cannot have millions of breakers in memory. An unbounded dictionary of state is a memory leak.</p>
<p><code>PolicyScope</code> is bounded by construction - there is no unbounded mode, because every entry carries a breaker and a budget. You can set <code>maximumKeys</code> when you create the scope (default 1024). Eviction is approximate: keys seen since the last sweep survive the next one, so the scope can briefly exceed its cap, but no lookup ever waits on a sweep.</p>
<p>If a tenant is evicted and then returns, they get a fresh policy with a fresh breaker. This means the &quot;memory&quot; of their failure is discarded, but it also ensures your process memory stays bounded regardless of how many unique tenants you serve. Size <code>maximumKeys</code> above the number of tenants you expect to be active at once.</p>
<h2>Advanced Shaping</h2>
<p>Sometimes, &quot;one size fits all&quot; isn&#39;t enough. You might have a &quot;Premium&quot; tenant who is allowed a longer deadline or a higher retry count than &quot;Free&quot; tenants.</p>
<p>You can pass a <code>shape</code> function to the <code>PolicyScope</code> constructor to customize the policy for each key on first sight:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">private</span><span class="tok"> </span><span class="tok-keyword">static</span><span class="tok"> </span><span class="tok-keyword">readonly</span><span class="tok"> </span><span class="tok-method">PolicyScope</span><span class="tok-punct">&lt;</span><span class="tok-keyword">string</span><span class="tok-punct">&gt;</span><span class="tok"> </span><span class="tok-type">Tenants</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">new</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">    template</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok">    shape</span><span class="tok-punct">:</span><span class="tok"> tenantId </span><span class="tok-punct">=&gt;</span><span class="tok"> tenantId</span><span class="tok-punct">.</span><span class="tok-method">StartsWith</span><span class="tok-punct">(</span><span class="tok-string">&quot;premium_&quot;</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">?</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span><span class="tok"> </span><span class="tok-punct">{</span><span class="tok"> </span><span class="tok-type">Deadline</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">TimeSpan</span><span class="tok-punct">.</span><span class="tok-method">FromSeconds</span><span class="tok-punct">(</span><span class="tok-number">30</span><span class="tok-punct">)</span><span class="tok"> </span><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok-punct">);</span>
</span></code></pre><p>Because the shaping happens once and the result is cached, this adds zero overhead to the hot path.</p>
<h2>Summary</h2>
<p>Resilience is about isolation. By moving from a single global policy to a keyed <code>PolicyScope</code>, you ensure that failures are contained. You protect your healthy tenants from your noisy ones, and you ensure that your resilience state is as granular as your architecture requires.</p>
<p>The built-in policies cover most cases, but real-world dependencies sometimes need custom logic. <a href="https://nresilience.net/blog/bending-the-library">Bending the library</a> covers the extension points - <code>Admit</code>, <code>BeforeAttempt</code>, <code>Backoff.Custom</code>, and <code>OnEvent</code> - that let you inject behavior without breaking the flat executor.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Streams that fail mid-flight</title>
      <link>https://nresilience.net/blog/streams-fail-mid-flight</link>
      <guid isPermaLink="true">https://nresilience.net/blog/streams-fail-mid-flight</guid>
      <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
      <description>Learn how NResilience handles IAsyncEnumerable sources, protecting the connection start, bounding a quiet stream, and resuming from a checkpoint when you have one.</description>
      <category>Power features</category>
      <category>streaming</category>
      <category>cancellation</category>
      <category>checkpointing</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> When you run a stream through a resilience policy, &quot;an attempt&quot; ends at the first element. NResilience protects the start of the stream - retrying if the connection fails - but once the first element arrives, the retry loop is over and mid-stream faults are yours to handle, unless you can tell the policy where the stream picked up. Two things survive the handoff: a stream that goes quiet for longer than the <code>AttemptTimeout</code> is cut off rather than left to hang, and a stream you can checkpoint restarts itself from the last element it delivered instead of ending your enumeration.</p>
<h2>The Streaming Dilemma</h2>
<p>Resilience is easy for a single value. You call a method, you get a result, or you get an exception. You can retry that whole cycle because the operation is atomic.</p>
<p>But a stream - an <code>IAsyncEnumerable&lt;T&gt;</code> - is not atomic. It&#39;s a conversation.</p>
<p>If you have a stream that yields 1,000 items, and the connection drops on item 500, what does &quot;retry&quot; even mean? Do you restart the entire stream from item 1? Do you try to resume from item 501? Unless the dependency supports a cursor or an offset, &quot;retrying&quot; a mid-flight failure is impossible for the library to do on your behalf - it has no way to know that offset 501 exists, let alone how to ask for it. Below is what NResilience does with the failure it <em>can</em> handle safely, and then the escape hatch for the one it can&#39;t guess: telling it the cursor yourself.</p>
<h2>The &quot;First Element&quot; Rule</h2>
<p>NResilience handles this by defining the boundary of an attempt as the <strong>time-to-first-element</strong>.</p>
<p>When you use <code>RunAsync</code> over a stream, the executor treats the process of obtaining the first element as the unit of work. </p>
<ol>
<li><strong>The Start:</strong> The executor calls your source and awaits the first <code>MoveNextAsync()</code>.</li>
<li><strong>The Protection:</strong> If the stream fails to start (e.g., a <code>SocketException</code> or a deadline), the executor treats it as a failed attempt. It applies the <code>Classifier</code>, checks the <code>RetryBudget</code>, and retries the <em>entire</em> stream start. A failed start throws from the first <code>MoveNextAsync</code> - the same exceptions a failed call throws, attempt log attached.</li>
<li><strong>The Handoff:</strong> The moment the first element is successfully yielded, the attempt is marked as <code>Ok</code>. The executor has done its job. It now hands the remaining stream over to you.</li>
</ol>
<p>Once that first element is in your hand, the resilience loop is closed. If the stream faults on the 10th or 100th element, that exception propagates directly to you.</p>
<h2>Why this is the only sane way</h2>
<p>You might ask: &quot;Why not protect the whole stream?&quot;</p>
<p>Imagine a policy with 3 retries and a 30-second deadline. If the stream yields 10 elements and then fails, and the library &quot;retried&quot; it, you would receive:
<code>Element 1, Element 2 ... Element 10, (Failure), Element 1, Element 2...</code></p>
<p>This is a nightmare for the consumer. You&#39;d have to implement complex deduplication logic just to handle a retry. By limiting the protection to the <em>start</em> of the stream, NResilience ensures that you never receive duplicate data, and the dependency isn&#39;t hammered by repeated requests for the same data.</p>
<h2>A fault is yours, silence is not</h2>
<p>One thing survives the handoff, and it is worth being precise about, because &quot;the resilience loop is closed&quot; used to mean rather more than it should have.</p>
<p>If the stream <em>faults</em> on element 100, that is yours: the exception propagates, and the library has no honest way to retry it. But if the stream goes <strong>quiet</strong> on element 100 - connection open, server still holding it, nothing arriving - there is nothing to propagate, and there used to be nothing to stop it either. No deadline covered it, because the attempt ended at element one. You got a stream that never yielded and never threw.</p>
<p><code>AttemptTimeout</code> now bounds the gap between two elements, and it is on by default. A pull that has been outstanding for longer than that throws an <code>AttemptStalledException</code> at your <code>MoveNextAsync</code>, carrying the bound and how many elements you had received:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-keyword">foreach</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-keyword">var</span><span class="tok"> trade </span><span class="tok-keyword">in</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">ct </span><span class="tok-punct">=&gt;</span><span class="tok"> feed</span><span class="tok-punct">.</span><span class="tok-method">SubscribeAsync</span><span class="tok-punct">(</span><span class="tok">ct</span><span class="tok-punct">),</span><span class="tok"> token</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-method">Handle</span><span class="tok-punct">(</span><span class="tok">trade</span><span class="tok-punct">);</span><span class="tok"> </span><span class="tok-comment">// an AttemptStalledException here means the feed went quiet, not that it broke</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><p>It bounds the gap rather than the total, so a stream that is slow but alive runs as long as it likes. What gets measured is how long a <em>pending pull</em> has been waiting on the far side, so a consumer that spends a minute processing each element is never blamed for the pause. And it is a bound, not a retry: the handoff already happened, so this ends your enumeration rather than restarting it.</p>
<p>That is still the &quot;First Element&quot; rule. It just no longer means silence is indistinguishable from success.</p>
<h2>When you <em>can</em> say where it picked up</h2>
<p>Resuming mid-stream - element 101 after a failure at 100 - needs something only you can supply: a cursor, an offset, a continuation token. When you have one, <code>RunAsync</code> and <code>TryRunAsync</code> take an overload for it, and the &quot;First Element&quot; rule above only ever applied to the <em>first</em> segment.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-keyword">foreach</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-keyword">var</span><span class="tok"> trade </span><span class="tok-keyword">in</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span>
</span><span class="md-code-line"><span class="tok">                   </span><span class="tok-punct">(</span><span class="tok">offset</span><span class="tok-punct">,</span><span class="tok"> ct</span><span class="tok-punct">)</span><span class="tok"> </span><span class="tok-punct">=&gt;</span><span class="tok"> feed</span><span class="tok-punct">.</span><span class="tok-method">SubscribeAsync</span><span class="tok-punct">(</span><span class="tok">fromOffset</span><span class="tok-punct">:</span><span class="tok"> offset</span><span class="tok-punct">,</span><span class="tok"> ct</span><span class="tok-punct">),</span>
</span><span class="md-code-line"><span class="tok">                   start</span><span class="tok-punct">:</span><span class="tok"> lastOffset</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok">                   checkpoint</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-keyword">static</span><span class="tok"> trade </span><span class="tok-punct">=&gt;</span><span class="tok"> trade</span><span class="tok-punct">.</span><span class="tok-type">Offset</span><span class="tok"> </span><span class="tok-punct">+</span><span class="tok"> </span><span class="tok-number">1</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-method">Handle</span><span class="tok-punct">(</span><span class="tok">trade</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><p>A connection reset after offset 500 re-invokes the source with <code>501</code>, the same way a failed <em>start</em> re-invokes it today - it goes through the same <code>Attempts</code>, the same backoff, the same classifier. What&#39;s different is that it can happen after the first element too, because now there is a way to say where &quot;again&quot; means. Nothing already delivered is delivered twice, because you defined both what a checkpoint is and how to read one off an element - the library never has to guess.</p>
<p>Two guardrails come with it. <code>Resilience.Restarts</code> - three by default - caps how many times one call may reconnect, separately from <code>Attempts</code>, so a feed that delivers one trade and then drops forever can&#39;t restart forever. And a restart is charged like a retry: it draws on the same <code>RetryBudget</code>, is admitted through the same <code>Breaker</code>, and is bounded by the deadline you set for the whole call, not a fresh window every time the feed reconnects.</p>
<p>The same idea covers an HTTP download you read yourself: <code>HttpResilienceOptions.ResumeDownloads</code> resumes a stalled body with a <code>Range</code> request instead of ending your read, when the server&#39;s first response made that safe to do - a strong <code>ETag</code> and <code>Accept-Ranges: bytes</code>, checked back with <code>If-Range</code> so a changed file is never spliced onto what you already had.</p>
<h2>Code in Action</h2>
<p>Using a resilient stream looks almost identical to a standard call:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-comment">// The source is cold: each attempt re-invokes it, the way the HTTP handler</span>
</span><span class="md-code-line"><span class="tok-comment">// builds a fresh request per attempt.</span>
</span><span class="md-code-line"><span class="tok-method">IAsyncEnumerable</span><span class="tok-punct">&lt;</span><span class="tok-type">Data</span><span class="tok-punct">&gt;</span><span class="tok"> stream </span><span class="tok-punct">=</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">RunAsync</span><span class="tok-punct">(</span><span class="tok">ct </span><span class="tok-punct">=&gt;</span>
</span><span class="md-code-line"><span class="tok">    client</span><span class="tok-punct">.</span><span class="tok-method">GetStreamingDataAsync</span><span class="tok-punct">(</span><span class="tok">url</span><span class="tok-punct">,</span><span class="tok"> ct</span><span class="tok-punct">),</span><span class="tok"> cancellationToken</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">try</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-keyword">foreach</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-keyword">var</span><span class="tok"> item </span><span class="tok-keyword">in</span><span class="tok"> stream</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-comment">// Once we are here, the first element has arrived.</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-comment">// The resilience loop is closed.</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-method">Process</span><span class="tok-punct">(</span><span class="tok">item</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok-keyword">catch</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-type">Exception</span><span class="tok"> ex</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// This is either a failure to start (if 0 items were yielded)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// or a mid-flight fault.</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><h2>Streams without the try/catch</h2>
<p>The example above still catches, because a stream that never starts throws from the first <code>MoveNextAsync</code>. If you&#39;d rather branch than catch, <code>TryRunAsync</code> takes a source too, and it answers the only question a stream can be asked before you consume it: <em>did the policy get it started?</em></p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> result </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">await</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-method">TryRunAsync</span><span class="tok-punct">(</span><span class="tok">ct </span><span class="tok-punct">=&gt;</span>
</span><span class="md-code-line"><span class="tok">    client</span><span class="tok-punct">.</span><span class="tok-method">GetStreamingDataAsync</span><span class="tok-punct">(</span><span class="tok">url</span><span class="tok-punct">,</span><span class="tok"> ct</span><span class="tok-punct">),</span><span class="tok"> cancellationToken</span><span class="tok-punct">);</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">if</span><span class="tok"> </span><span class="tok-punct">(!</span><span class="tok">result</span><span class="tok-punct">.</span><span class="tok-method">TryGetValue</span><span class="tok-punct">(</span><span class="tok">out </span><span class="tok-keyword">var</span><span class="tok"> stream</span><span class="tok-punct">))</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// The policy never got it started. Reason, Exception and the attempt log</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// are all on the result - the same failure RunAsync would have thrown.</span>
</span><span class="md-code-line"><span class="tok">    _logger</span><span class="tok-punct">.</span><span class="tok-method">LogWarning</span><span class="tok-punct">(</span><span class="tok-string">&quot;The feed never opened: {Reason}&quot;</span><span class="tok-punct">,</span><span class="tok"> result</span><span class="tok-punct">.</span><span class="tok-type">Reason</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">return</span><span class="tok-punct">;</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-keyword">foreach</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok-keyword">var</span><span class="tok"> item </span><span class="tok-keyword">in</span><span class="tok"> stream</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-method">Process</span><span class="tok-punct">(</span><span class="tok">item</span><span class="tok-punct">);</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><p>The await ends at the first element. By the time that line returns, every attempt, every backoff and every guard has already run - which is exactly the window this post is about, expressed as a result instead of an exception. A fault at item 500 still throws from <code>MoveNextAsync</code>, because the result was decided before item 500 existed.</p>
<p>There is one rule that comes with it, and it is the only one like it in the library: a successful result <strong>owns a live enumerator</strong>. Its first element has already been pulled, so enumerate it once, or dispose it if you change your mind:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">if</span><span class="tok"> </span><span class="tok-punct">(</span><span class="tok">result</span><span class="tok-punct">.</span><span class="tok-type">IsSuccess</span><span class="tok"> </span><span class="tok-punct">&amp;&amp;</span><span class="tok"> </span><span class="tok-punct">!</span><span class="tok">stillNeeded</span><span class="tok-punct">)</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-keyword">await</span><span class="tok"> </span><span class="tok-punct">((</span><span class="tok-type">IAsyncDisposable</span><span class="tok-punct">)</span><span class="tok">result</span><span class="tok-punct">.</span><span class="tok-type">Value</span><span class="tok-punct">!).</span><span class="tok-method">DisposeAsync</span><span class="tok-punct">();</span>
</span><span class="md-code-line"><span class="tok-punct">}</span>
</span></code></pre><p>The alternatives are worse. Reporting success before the first element would report it exactly when a streaming call actually fails, and buffering the element so the result could be safely dropped would charge every stream for the rare caller who abandons one.</p>
<h2>Constraints: The Hedge Problem</h2>
<p>There is one important limitation: <strong>you cannot hedge a stream</strong>.</p>
<p>Hedging works by starting a second copy of a call and taking whichever answer arrives first. For a value, this is easy. For a stream, it&#39;s impossible without massive buffering. If a hedge &quot;won&quot; on the second attempt, you&#39;d have to discard the first attempt&#39;s progress and restart the stream, or buffer the second attempt&#39;s items until the first one was cancelled.</p>
<p>If you pass a hedged policy to a streaming <code>RunAsync</code> or <code>TryRunAsync</code> overload, NResilience refuses it at the call with a configuration exception rather than silently doing nothing. The same policy still runs ordinary calls.</p>
<h2>Summary</h2>
<p>Streaming resilience is about protecting the connection, not the data. Time-to-first-element is what gets retried by default, which keeps your data linear and duplicate-free; the gap between elements is what gets bounded, which is what actually stops a &quot;zombie&quot; connection from hanging your app. If you can hand the policy a checkpoint, a mid-stream fault stops being yours alone to handle - it restarts from where you left off instead. One detail worth knowing for your dashboards: what the breaker samples is time to the first element, so a stream that opens fast and dies later reads as a fast success to the breaker - a stall shows up as <code>nresilience.stalls</code>, and a restart as <code>nresilience.stream_resumed</code>.</p>
<p>When you have multiple tenants sharing a dependency, you need per-tenant isolation. <a href="https://nresilience.net/blog/one-policy-per-tenant">One policy per tenant</a> introduces <code>PolicyScope</code> and how to avoid the noisy neighbor problem.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Race the tail</title>
      <link>https://nresilience.net/blog/race-the-tail</link>
      <guid isPermaLink="true">https://nresilience.net/blog/race-the-tail</guid>
      <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
      <description>Stop letting a few slow requests ruin your p99. Learn how hedging sends a duplicate request to &quot;race the tail&quot; and cut latency spikes.</description>
      <category>Power features</category>
      <category>hedging</category>
      <category>latency</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Tail latency - the slowest 1% of your calls - is often caused by a single &quot;zombie&quot; request. Hedging fixes this by starting a second copy of the call if the first one takes longer than almost every other call. By taking whichever answer arrives first, you shorten your p99 for the cost of a small, bounded increase in load.</p>
<h2>The Tyranny of the Tail</h2>
<p>If you look at a latency histogram, most of your calls are fast. But then there&#39;s the tail: those few requests that take 10x the normal time. </p>
<p>In a microservices architecture, the tail is a contagion. If your frontend calls five services, and each has a p99 of 1 second, your frontend&#39;s p99 is effectively the sum of those tails. One slow request at the bottom of the stack can make the entire user experience feel broken.</p>
<p>The traditional answer is to set a timeout. But if you time out at 1 second, you just turn a slow success into a failure. You&#39;ve solved the latency, but you&#39;ve broken the call.</p>
<h2>Enter Hedging: Racing the Tail</h2>
<p>Hedging is the a-ha moment for tail latency. Instead of waiting for a request to fail, you simply start another one.</p>
<p>If a call is taking longer than almost every other call to that dependency, it&#39;s probably stuck. By starting a second attempt now, you get two draws from the latency distribution. The chance that <em>both</em> draws land in the 99th percentile is 1 in 10,000. You&#39;ve effectively traded a tiny bit of extra load for a massive reduction in p99.</p>
<h2>The Safety Problem: Why Quantiles Matter</h2>
<p>The biggest fear with hedging is the &quot;Load Spike.&quot; If you set a fixed hedge delay of 100ms, and your dependency suddenly slows down so that <em>every</em> call takes 110ms, you&#39;ve just doubled your traffic to a service that is already struggling. You&#39;ve turned a brownout into an outage.</p>
<p>NResilience solves this by making the hedge threshold a <strong>live quantile</strong>.</p>
<p>When you configure <code>Hedge.At(0.95)</code>, the library tracks the recent p95 latency. The second request only fires if the first one exceeds that p95. </p>
<p>Because the quantile moves <em>with</em> the traffic, the fraction of calls that hedge stays constant (roughly 5%) regardless of whether the dependency is responding in 10ms or 10 seconds. The load on the dependency is stable, and the protection is constant.</p>
<h2>The Hedging Gates</h2>
<p>To ensure hedging only happens when it&#39;s safe and useful, NResilience applies several guards:</p>
<ol>
<li><strong>The Baseline Gate:</strong> Hedging is disabled until the latency estimate has enough samples (<code>MinimumSamples</code>, default 20) to be reliable. A cold policy hedges nothing.</li>
<li><strong>The Concurrency Gate:</strong> <code>MaximumConcurrent</code> bounds how many attempts may be in flight at once, counting the first (default 2). This prevents a single call from spawning an army of duplicates.</li>
<li><strong>The Health Gate (<code>SuppressAt</code>):</strong> Hedging stops when the dependency&#39;s error rate reaches a fraction of the breaker&#39;s trip point (half, by default). Sending extra load to a failing service is a mistake.</li>
<li><strong>The Value Gate (<code>WinRate</code>):</strong> Opt-in. If the second request almost never wins the race, the policy retreats and hedges less; when hedges start winning again, it adds hedging back.</li>
<li><strong>The Criticality Gate:</strong> Opt-in, through <code>UseAmbientCriticality</code>. A call running at <code>Criticality.Sheddable</code> is never hedged, whatever the estimate says. Hedging spends capacity to buy latency, and there is no latency worth buying when nobody is waiting for the answer. See <a href="https://nresilience.net/blog/deadlines-that-travel">Deadlines that travel</a> for how a level reaches the policy in the first place.</li>
</ol>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> policy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Attempts</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">3</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Hedge</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Hedge</span><span class="tok-punct">.</span><span class="tok-method">At</span><span class="tok-punct">(</span><span class="tok">quantile</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">0.95</span><span class="tok-punct">)</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-type">MaximumConcurrent</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">2</span><span class="tok-punct">,</span>
</span><span class="md-code-line"><span class="tok">        </span><span class="tok-type">SuppressAt</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-number">0.5</span><span class="tok"> </span><span class="tok-comment">// stop hedging halfway to a breaker trip (the default)</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-punct">}</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><h2>Watching the Threshold</h2>
<p>Because the threshold moves, it is a number worth watching rather than a number you set once. Read it straight off the policy:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> armsAt </span><span class="tok-punct">=</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-type">Measured</span><span class="tok-punct">.</span><span class="tok-type">HedgeThreshold</span><span class="tok-punct">;</span><span class="tok">   </span><span class="tok-comment">// null until the estimate has MinimumSamples</span>
</span></code></pre><p>That is the latency at which the library starts duplicating load, so it belongs on the same dashboard as the extra traffic it explains. In <code>NResilience.Extensions</code> the same figure arrives as <code>nresilience.hedge.threshold</code>, recorded each time a hedge fires; the property is the one that answers before a hedge has fired.</p>
<p>One caveat: the gates above are asked when the threshold fires, not when you read it. A non-null reading is the point at which a hedge <em>would</em> be considered, not a promise that one starts.</p>
<p>A second caveat about what the quantile is measuring. It is wall clock around your callback, so a thread pool deep enough to add 400ms to every call raises the threshold too, and hedges stop firing during exactly the incident where a second copy of the work is the wrong answer anyway. Setting <code>Saturation = Saturation.Above(5)</code> stops the estimate learning that - see <a href="https://nresilience.net/blog/measured-not-guessed">Measured, not guessed</a> for the mechanism.</p>
<h2>Cost Accounting: When to Hedge</h2>
<p>Hedging isn&#39;t free. You are spending extra CPU, memory, and network bandwidth. It&#39;s a trade-off: is a 5% increase in load worth a 90% reduction in p99?</p>
<p>For most &quot;read&quot; operations (GET requests), the answer is yes. For &quot;write&quot; operations (POST/PUT), you must ensure the operation is <strong>idempotent</strong> - meaning the dependency can handle the same request twice without side effects. NResilience handles this via the <code>MarkRepeatable()</code> flag, ensuring only safe calls are hedged.</p>
<h2>Summary</h2>
<p>Hedging is the most effective tool for killing the tail. By racing a duplicate request against a slow one, you route around transient glitches and zombie requests. Because NResilience uses live quantiles instead of constants, you get the benefits of a shorter tail without the risk of a retry storm.</p>
<p>Hedging works well for single-value calls, but streams have different constraints. <a href="https://nresilience.net/blog/streams-fail-mid-flight">Streams that fail mid-flight</a> explains why protection is limited to the time-to-first-element and what that means for <code>IAsyncEnumerable</code>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Measured, not guessed</title>
      <link>https://nresilience.net/blog/measured-not-guessed</link>
      <guid isPermaLink="true">https://nresilience.net/blog/measured-not-guessed</guid>
      <pubDate>Fri, 31 Jul 2026 00:00:00 GMT</pubDate>
      <description>Stop guessing your timeouts and retry counts. Learn how NResilience uses adaptive bounds to measure dependency latency and error rates in real time.</description>
      <category>Protecting the dependency</category>
      <category>adaptive</category>
      <category>timeouts</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> The hardest part of resilience is picking the right numbers. NResilience replaces guesswork with measurement: it tracks recent p95 latency and error rates to automatically tighten your bounds, ensuring you fail fast during a brownout without manually tuning constants for every dependency.</p>
<h2>The &quot;Magic Number&quot; Problem</h2>
<p>Every experienced engineer has a folder of &quot;magic numbers&quot; in their config files:</p>
<ul>
<li><code>PaymentApi_Timeout: 2s</code></li>
<li><code>InventoryApi_RetryCount: 3</code></li>
<li><code>SearchApi_BreakThreshold: 0.1</code></li>
</ul>
<p>The problem is that these numbers are almost always wrong. A 2-second timeout might be perfect on Tuesday, but if the dependency&#39;s p99 latency climbs to 2.1 seconds on Wednesday, your app suddenly starts failing every single request - even though the dependency is still healthy, just slightly slower.</p>
<p>If you set the numbers too high, you&#39;re not protected against brownouts. If you set them too low, you&#39;re killing healthy traffic.</p>
<h2>The Rule: Constant and Measured</h2>
<p>NResilience solves this with a single, unifying rule: <strong>Bounds can be stated as constants or measured from the dependency, and when both exist, the tighter one wins.</strong></p>
<p>The measured term never loosens what you wrote. A <code>Deadline</code> is pure constant - measurement never touches it, so a 10-second deadline is a 10-second deadline whatever the dependency does. What measurement tightens are the bounds that sit inside it: the per-attempt ceiling, the breaker&#39;s trip points. If the dependency suddenly slows down, the measured term kicks in and fails a call at 2 seconds, long before your &quot;safe&quot; 10-second constant is reached.</p>
<h2>The Three Adaptive Pairs</h2>
<p>The library applies this logic to the three most critical areas of resilience.</p>
<h3>1. The Attempt Ceiling</h3>
<p>Instead of a fixed <code>AttemptTimeout</code>, the library tracks the recent p95 latency of successful calls. By default, it sets the ceiling at <strong>3x the recent p95</strong>.</p>
<p>If the dependency normally responds in 100ms, your attempt timeout is effectively 300ms. If it slows to 500ms, your timeout automatically shifts to 1.5s. You get the protection of a tight timeout without the fragility of a constant.</p>
<h3>2. Adaptive Breakers</h3>
<p>Circuit breakers usually trip on a fixed error ratio (e.g., 10%). But for a high-reliability API, a 10% error rate is a catastrophe; for a flaky legacy system, it&#39;s a Tuesday.</p>
<p>NResilience measures the <em>normal</em> error rate of the dependency. It then trips the breaker if the current rate spikes to a multiple of that baseline (e.g., 5x normal). The breaker adapts to the &quot;noise floor&quot; of the dependency.</p>
<h3>3. Adaptive Concurrency (The Bulkhead)</h3>
<p>Picking a concurrency limit (bulkhead) is a guessing game based on how many pods you have. If you have 10 pods and a dependency that handles 1,000 concurrent calls, you set a limit of 100. But what happens when you scale to 50 pods? You&#39;ve just quadrupled the load on the dependency.</p>
<p><code>Limit.Adaptive</code> discovers the limit by watching for queueing. When latency spikes while the limit is high, the limit shrinks. When latency is stable, it probes upward. The limit stays correct regardless of your pod count.</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-comment">// No permit count to pick. The ceiling is the one number worth setting per dependency:</span>
</span><span class="md-code-line"><span class="tok-comment">// it is what the loop cannot exceed when the measurement is wrong.</span>
</span><span class="md-code-line"><span class="tok-keyword">using</span><span class="tok"> </span><span class="tok-keyword">var</span><span class="tok"> limiter </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Limit</span><span class="tok-punct">.</span><span class="tok-method">Adaptive</span><span class="tok-punct">(</span><span class="tok">maximum</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">200</span><span class="tok-punct">);</span>
</span></code></pre><p>Every parameter is optional - <code>Limit.Adaptive()</code> on its own is a complete limiter - and the rest of the knobs (the floor, how much slower than baseline counts as queueing, how hard a congested round backs off) live on an <code>AdaptiveLimitOptions</code> you pass as <code>options</code> when you want them.</p>
<h2>From Cold Start to Steady State</h2>
<p>You might wonder: &quot;If the library measures latency, what happens when the app first starts?&quot;</p>
<p>A &quot;cold&quot; policy behaves exactly like a constants-only process. The measured terms stay invisible until they have a sufficient baseline of samples (the <code>MinimumSamples</code> threshold). </p>
<ol>
<li><strong>Cold Start:</strong> Only your constants (e.g., <code>Deadline = 30s</code>) are enforced.</li>
<li><strong>Warming Up:</strong> The library samples successful attempts.</li>
<li><strong>Steady State:</strong> The measured bounds kick in, tightening the ceiling around the actual behavior of the dependency.</li>
</ol>
<h2>When the Measurement Measures the Wrong Thing</h2>
<p>Everything above rests on one assumption, and it is worth saying out loud: that the number being measured is a fact about the dependency.</p>
<p>It is measured with a wall clock around your callback. So it is really a fact about <em>everything</em> between calling the dependency and getting an answer back - and one of those things is your own thread pool.</p>
<p>Picture a process whose pool is starved. Work items are queuing 400ms before they reach a thread. From inside the executor that is indistinguishable from a dependency that got 400ms slower, and every measured term responds accordingly:</p>
<ul>
<li>The attempt ceiling rises, so a doomed attempt runs longer.</li>
<li>The measured backoff base lengthens, so the retry that would have worked waits.</li>
<li>The hedge threshold climbs out of reach, so hedges stop firing.</li>
</ul>
<p>All three loosen, at the same moment, and the moment is a local incident. Meanwhile the breaker sits closed and the retry budget sits full, because both of them sample the dependency - and the dependency is fine.</p>
<p>That is a defect in the adaptive story rather than a missing feature, and thread-pool starvation is one of the most common causes of a .NET outage. So there is a switch for it:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> api </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Http</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">AttemptCeiling</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">AttemptCeiling</span><span class="tok-punct">.</span><span class="tok-method">Above</span><span class="tok-punct">(</span><span class="tok">multiple</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">3</span><span class="tok-punct">),</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// Five times this process's own normal queue delay. Above that, the three terms above stop</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-comment">// being fed - they hold what they last learned until the queue drains.</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Saturation</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Saturation</span><span class="tok-punct">.</span><span class="tok-method">Above</span><span class="tok-punct">(</span><span class="tok">multiple</span><span class="tok-punct">:</span><span class="tok"> </span><span class="tok-number">5</span><span class="tok-punct">),</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p><code>Saturation.Above(5)</code> is dimensionless on purpose, like every other number in this post. &quot;Five times its own normal&quot; survives being copied to a pod with a different core count; &quot;200ms&quot; does not. A <code>Floor</code> of 20ms sits under it, because a healthy pool queues in microseconds and five times microseconds is still microseconds - nobody wants a policy that stops learning every time the garbage collector moves one probe.</p>
<p>What it does is narrow, and deliberately so: <strong>it only declines to record.</strong> Nothing is refused, no bound moves, no delay is added. A policy that finds itself saturated makes the same attempts, in the same shape, with the same verdicts. The worst case is behaving exactly as it does without the switch, which is what makes it safe to turn on.</p>
<p>The measurement behind it is one process-wide probe: a work item stamped with a timestamp, queued into the global pool queue, measured when it runs. Four times a second, read as a couple of volatile loads, and a process that never configures this never queues one. And when the pool is deep enough that the probe itself cannot run, the probe&#39;s own overdue-ness is the reading - which is the whole trick, because anything that waited for the callback would report the microsecond it measured before the incident started.</p>
<p>It is off by default, and not because it could do harm. It changes what every other measured term in the policy learns, and a change of that shape earns its own deliberate rollout.</p>
<p>One honest failure mode, stated the way the adaptive limiter states its own: a process that is <em>always</em> saturated learns a high queue delay as normal and stops finding itself saturated. The baseline is relative, so all it can tell you is that this minute is unlike the last hour. It keeps a local incident from corrupting your dependency measurements. It is not a substitute for having enough threads.</p>
<h2>Reading What It Measured</h2>
<p>An adaptive bound you cannot see is an adaptive bound you cannot trust. Every measured term the policy holds is readable from one property:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> reading </span><span class="tok-punct">=</span><span class="tok"> policy</span><span class="tok-punct">.</span><span class="tok-type">Measured</span><span class="tok-punct">;</span>
</span><span class="md-code-line">
</span><span class="md-code-line"><span class="tok">reading</span><span class="tok-punct">.</span><span class="tok-type">AttemptCeiling</span><span class="tok-punct">;</span><span class="tok">   </span><span class="tok-comment">// what the ceiling currently measures to, before AttemptTimeout clamps it</span>
</span><span class="md-code-line"><span class="tok">reading</span><span class="tok-punct">.</span><span class="tok-type">BackoffBase</span><span class="tok-punct">;</span><span class="tok">      </span><span class="tok-comment">// the base delay the next transient retry would wait, after the spread clamp</span>
</span><span class="md-code-line"><span class="tok">reading</span><span class="tok-punct">.</span><span class="tok-type">HedgeThreshold</span><span class="tok-punct">;</span><span class="tok">   </span><span class="tok-comment">// the latency at which a hedge arms - where extra load starts</span>
</span><span class="md-code-line"><span class="tok">reading</span><span class="tok-punct">.</span><span class="tok-type">QueueDelay</span><span class="tok-punct">;</span><span class="tok">       </span><span class="tok-comment">// how long a work item waits for a thread in this process's pool</span>
</span></code></pre><p>Each one is <code>null</code> until its feature is configured <em>and</em> its estimate is warm, which is the cold-start behaviour above, stated as a value you can check. They are readings, never configuration: the knobs that produce them are <code>AttemptCeiling</code>, <code>Backoff.MeasuredBase</code>, <code>Hedge</code> and <code>Saturation</code>, and those keep the plain nouns.</p>
<p>For the whole picture at once, <code>policy.Explain()</code> prints them beside the configuration that produces them, and labels each one cold or warm:</p>
<pre class="md-code md-code-text"><code><span class="md-code-line"><span class="tok">  Measured now:    attempt ceiling   1.97s  3x p95 over 5m, 20 samples minimum</span>
</span><span class="md-code-line"><span class="tok">                   backoff base      -      not configured</span>
</span><span class="md-code-line"><span class="tok">                   hedge threshold   -      cold - p95 over 30s, 20 samples minimum</span>
</span><span class="md-code-line"><span class="tok">                   pool queue delay  -      not configured</span>
</span></code></pre><p>Three states, told apart: a warm reading, a term nobody configured, and a configured term that has not seen enough traffic to say anything yet. That last distinction is the one every support question turns on. &quot;Configured&quot; and &quot;in effect&quot; are different facts, and a policy that reports only the first one is a policy you are reading half of.</p>
<p><code>Measured.HedgeThreshold</code> is the one worth putting on a dashboard first. It is the latency at which the library starts duplicating requests, so it explains the extra traffic beside it - and because it is a live quantile, watching it move during an incident tells a brownout from a tail.</p>
<p>Put <code>Measured.QueueDelay</code> next to it. It is the only reading here that describes your process rather than the dependency, and when it moves with the other two, the dependency did not get slower and this host did. The <code>nresilience.pool.delay</code> histogram records the same number at the onset of each episode, so a count of samples is a count of local incidents.</p>
<p>The estimates are private to the policy instance. The HTTP handler derives one policy per host, so each host is measured on its own and a slow host never tightens a fast one&#39;s ceiling. <code>QueueDelay</code> is the exception: there is one thread pool, so every policy in the process reports the same number for it.</p>
<h2>Opting Out</h2>
<p>Some scenarios require absolute determinism, like a strict performance benchmark. You can disable all adaptive behavior with a single switch:</p>
<pre class="md-code"><code><span class="md-code-line"><span class="tok-keyword">var</span><span class="tok"> deterministicPolicy </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-type">Resilience</span><span class="tok-punct">.</span><span class="tok-type">Default</span><span class="tok"> </span><span class="tok-keyword">with</span>
</span><span class="md-code-line"><span class="tok-punct">{</span>
</span><span class="md-code-line"><span class="tok">    </span><span class="tok-type">Adaptive</span><span class="tok"> </span><span class="tok-punct">=</span><span class="tok"> </span><span class="tok-keyword">false</span>
</span><span class="md-code-line"><span class="tok-punct">};</span>
</span></code></pre><p>Setting <code>Adaptive = false</code> on the policy turns off the measured attempt ceiling and every other measured term the library would supply. You are left with exactly the constants you wrote - no more, no less. <code>Saturation</code> is refused alongside it rather than ignored: it decides when to stop measuring, so a policy that measures nothing has nothing for it to do, and the library says so instead of picking a winner.</p>
<p>One boundary on the switch: it deliberately stops at the breaker. A breaker is a live object two policies may share, and a policy switch that silently muted a shared breaker would surprise whoever holds the other reference. Give the breaker&#39;s settings their own <code>Adaptive = false</code> if you want it constants-only too. In configuration there is no such sharing, and a single <code>&quot;Adaptive&quot;: false</code> JSON key covers both.</p>
<h2>Summary</h2>
<p>Resilience shouldn&#39;t be a game of &quot;guess the number.&quot; By composing constants with real-time measurements, NResilience gives you the safety of a hard ceiling and the efficiency of an adaptive one. You define the absolute limits of what you&#39;ll tolerate, and the library handles the nuance of the dependency&#39;s actual performance.</p>
<p>The most visible benefit of measured bounds is shorter tail latency. <a href="https://nresilience.net/blog/race-the-tail">Race the tail</a> explains how hedging uses live quantiles to send a duplicate request when the first one is slow, cutting p99 without the risk of a retry storm.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
