Introduction

Most articles stop at “add retry and circuit breaker”.

Real systems fail in complex, nonlinear ways—timeouts interact with retries, retries amplify load, and circuit breakers can become global bottlenecks.

.NET 8’s Resilience Pipelines are not just a Polly replacement; they represent a new execution model for fault handling in ASP.NET Core.

This article goes beyond configuration and explores:

This is production-grade resilience engineering.

Mental Model: How a Resilience Pipeline Executes

A resilience pipeline is a decorated execution chain:

Request
 ↓
Timeout
 ↓
Retry
 ↓
Circuit Breaker
 ↓
Hedging
 ↓
Actual Operation

Key insight:

Order matters more than configuration

Example mistake:

Strategy Ordering: The Correct Default

Recommended Order

Hedging
 → Timeout
   → Retry
     → Circuit Breaker
       → Operation

Why?

Building a Fully Controlled Pipeline

builder.Services.AddResiliencePipeline<HttpResponseMessage>(
    "advanced-pipeline",
    pipeline =>
    {
        pipeline
            .AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
            {
                MaxHedgedAttempts = 2,
                Delay = TimeSpan.FromMilliseconds(150)
            })
            .AddTimeout(TimeSpan.FromSeconds(2))
            .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
            {
                MaxRetryAttempts = 2,
                ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                    .Handle<HttpRequestException>()
                    .HandleResult(r => !r.IsSuccessStatusCode)
            })
            .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
            {
                FailureRatio = 0.4,
                MinimumThroughput = 20,
                SamplingDuration = TimeSpan.FromSeconds(30),
                BreakDuration = TimeSpan.FromSeconds(10)
            });
    });

This pipeline:

Keyed Pipelines: Tenant-Level Isolation (Advanced)

One of the least discussed but most powerful features is keyed pipelines.

Problem

Solution: Keyed Pipelines

builder.Services.AddResiliencePipeline<string, HttpResponseMessage>(
    "tenant-pipeline",
    (context, pipeline) =>
    {
        var tenantId = context;

        pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions
        {
            FailureRatio = tenantId == "premium" ? 0.7 : 0.3,
            BreakDuration = TimeSpan.FromSeconds(10)
        });
    });

Usage

await resiliencePipeline.ExecuteAsync(
    tenantId,
    async token => await httpClient.GetAsync(url, token));

✔ One tenant fails → others remain healthy
✔ Enterprise-grade isolation
✔ Zero gateway dependency

Per-Request Dynamic Resilience (Rarely Covered)

Sometimes resilience cannot be static.

Example:

Dynamic Override Using Context

var context = new ResilienceContext
{
    Properties =
    {
        ["AllowRetry"] = false
    }
};

await pipeline.ExecuteAsync(context, async token =>
{
    return await httpClient.PostAsync(url, content, token);
});

Custom Retry Predicate

ShouldHandle = args =>
{
    if (args.Context.Properties.TryGetValue("AllowRetry", out var allow) &&
        allow is false)
        return ValueTask.FromResult(false);

    return ValueTask.FromResult(true);
};

This enables business-aware resilience, not blind retries.

Writing a Custom Resilience Strategy (Ultra-Advanced)

Scenario

You want to:

Custom Strategy

public sealed class DeploymentBlockStrategy : ResilienceStrategy
{
    protected override async ValueTask<TResult> ExecuteCore<TResult>(
        Func<ResilienceContext, ValueTask<TResult>> callback,
        ResilienceContext context)
    {
        if (DeploymentState.IsInProgress)
            throw new InvalidOperationException("Deployment in progress");

        return await callback(context);
    }
}

Registering It

pipeline.AddStrategy(new DeploymentBlockStrategy());

This is framework-level extensibility, not middleware hacks.

Deterministic Testing of Failure Scenarios

Problem

Resilience code is notoriously hard to test.

Solution: Virtual Time & Controlled Failures

var attempts = 0;

var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3
    })
    .Build();

await Assert.ThrowsAsync<HttpRequestException>(() =>
    pipeline.ExecuteAsync(async _ =>
    {
        attempts++;
        throw new HttpRequestException();
    }));

Assert.Equal(4, attempts); // Initial + retries

You can now:

Observability: Signals That Actually Matter

Forget logging “retry happened.”

Track:

Hooking into Events

pipeline.OnRetry(args =>
{
    logger.LogWarning(
        "Retry {Attempt} due to {Reason}",
        args.AttemptNumber,
        args.Outcome.Exception?.Message);
});

Common Anti-Patterns (Seen in Production)

When Resilience Pipelines Change Architecture Decisions

With proper pipelines:

This isn’t just a library—it’s an architectural primitive.

Final Thoughts

.NET 8 Resilience Pipelines are:

Most developers will use 10% of their power.

The remaining 90% is where high-scale, fault-tolerant systems are built.

If you master this, you’re not just writing APIs—you’re designing resilient distributed systems.