Resilience
Polly v8 resilience patterns for .NET — retry, circuit breaker, timeout, hedging, and IHttpClientFactory integration using Microsoft.Extensions.Resilience.
Install
Quick install
npx skills add https://github.com/zdanovichnick/dotnet-pilotnpx skills add zdanovichnick/dotnet-pilot --agent claude-codenpx skills add zdanovichnick/dotnet-pilot --agent cursornpx skills add zdanovichnick/dotnet-pilot --agent codexnpx skills add zdanovichnick/dotnet-pilot --agent opencodenpx skills add zdanovichnick/dotnet-pilot --agent github-copilotnpx skills add zdanovichnick/dotnet-pilot --agent windsurfMore install options
Shorthand — useful for multi-skill repos:
npx skills add zdanovichnick/dotnet-pilotManual — clone the repo and drop the folder into your agent's skills directory:
git clone https://github.com/zdanovichnick/dotnet-pilot.gitcp -r dotnet-pilot ~/.claude/skills/Resilience
Polly v8 resilience patterns for .NET — retry, circuit breaker, timeout, hedging, and IHttpClientFactory integration using Microsoft.Extensions.Resilience.
---
name: resilience
description: Polly v8 resilience patterns for .NET — retry, circuit breaker, timeout, hedging, and IHttpClientFactory integration using Microsoft.Extensions.Resilience.
---
Resilience Patterns (Polly v8)
Reference for building fault-tolerant .NET services using Polly v8 and Microsoft.Extensions.Resilience. Used by dnp-planner and dnp-tdd-developer-hard.
Package Reference
<PackageReference Include="Microsoft.Extensions.Resilience" Version="9.*" />
<!-- For HTTP clients: -->
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.*" />
Breaking change from Polly v7: the Policy.Handle<>().Retry() API is gone. Use ResiliencePipelineBuilder exclusively.
Core Concepts
| Concept | Purpose |
|---------|---------|
| ResiliencePipeline | Executes a delegate through a chain of strategies |
| ResiliencePipelineBuilder | Fluent builder — add strategies in order (outermost first) |
| ResiliencePipelineProvider<TKey> | DI-resolved registry; resolve pipelines by key |
| ResilienceContext | Per-execution metadata (cancellation token, properties) |
Strategies execute in the order they are added. Outer strategies wrap inner ones — add timeout last to apply it per-attempt, or first to apply it to the whole pipeline.
DI Registration
builder.Services.AddResiliencePipeline("database", pipeline =>
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<SqlException>()
.Handle<TimeoutException>(),
OnRetry = args =>
{
// args.Context, args.AttemptNumber, args.Outcome available
Console.WriteLine($"Retry {args.AttemptNumber} after {args.RetryDelay}");
return ValueTask.CompletedTask;
}
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(15)
})
.AddTimeout(TimeSpan.FromSeconds(5)));
Retry Strategy
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
// Fixed, Linear, or Exponential
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromSeconds(1), // base delay; doubles each attempt with Exponential
// Adds ±20% random jitter to prevent thundering herd
UseJitter = true,
// Only retry on specific exceptions or results
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.TooManyRequests),
OnRetry = args =>
{
logger.LogWarning(
"Retry attempt {Attempt} after {Delay}ms due to {Exception}",
args.AttemptNumber,
args.RetryDelay.TotalMilliseconds,
args.Outcome.Exception?.Message);
return ValueTask.CompletedTask;
}
})
Circuit Breaker
Opens the circuit when the failure ratio exceeds the threshold, stopping all calls for BreakDuration.
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
// Open circuit if ≥50% of calls fail
FailureRatio = 0.5,
// Measurement window
SamplingDuration = TimeSpan.FromSeconds(30),
// Minimum calls in window before circuit can open
MinimumThroughput = 5,
// How long circuit stays open before moving to half-open
BreakDuration = TimeSpan.FromSeconds(15),
OnOpened = args =>
{
logger.LogError("Circuit opened for {Duration}s", args.BreakDuration.TotalSeconds);
return ValueTask.CompletedTask;
},
OnClosed = _ =>
{
logger.LogInformation("Circuit closed — service recovered");
return ValueTask.CompletedTask;
}
})
When the circuit is open, calls throw BrokenCircuitException immediately without hitting the dependency.
Timeout
// Per-attempt timeout (placed after retry — each attempt gets its own timeout)
.AddTimeout(TimeSpan.FromSeconds(5))
// Or with options for callbacks
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(5),
OnTimeout = args =>
{
logger.LogWarning("Operation timed out after {Timeout}", args.Timeout);
return ValueTask.CompletedTask;
}
})
Hedging (Parallel Fallback Requests)
Sends a duplicate request after a delay if the first hasn't returned. Uses the first successful response.
.AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
{
// Send a second request after 500ms if first hasn't returned
Delay = TimeSpan.FromMilliseconds(500),
// How many parallel hedged requests to allow
MaxHedgedAttempts = 2,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => !r.IsSuccessStatusCode),
ActionGenerator = args => () =>
ValueTask.FromResult(Outcome.FromResult(
args.PrimaryContext.Properties.GetValue(
new ResiliencePropertyKey<HttpResponseMessage>("hedged-result"),
null!)))
})
Use hedging for latency-sensitive read operations where idempotency is guaranteed.
IHttpClientFactory Integration
Standard Resilience Handler (recommended shortcut)
Applies retry + circuit breaker + timeout + rate limiter in one call:
builder.Services.AddHttpClient("payments", client =>
{
client.BaseAddress = new Uri(builder.Configuration["PaymentsApi:BaseUrl"]!);
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.FailureRatio = 0.5;
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});
Custom Pipeline per Client
builder.Services.AddHttpClient("inventory")
.AddResilienceHandler("inventory-pipeline", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 2,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
Consuming the Named Client
public class InventoryClient(IHttpClientFactory factory)
{
public async Task<InventoryResponse?> GetStockAsync(string sku, CancellationToken ct)
{
var client = factory.CreateClient("inventory");
var response = await client.GetAsync($"/stock/{sku}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<InventoryResponse>(ct);
}
}
Named Pipeline Usage (Non-HTTP)
public class OrderRepository(
AppDbContext db,
ResiliencePipelineProvider<string> pipelines,
ILogger<OrderRepository> logger)
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
{
var pipeline = pipelines.GetPipeline("database");
return await pipeline.ExecuteAsync(
async token => await db.Orders.FindAsync([id], token),
ct);
}
}
Recommended Pipeline Compositions
| Use Case | Strategies (outer → inner) |
|----------|---------------------------|
| Database queries | Retry (3x exponential) → Timeout (5s) |
| HTTP API calls | Retry (3x) → Circuit Breaker → Timeout (10s total) |
| Payment processing | Circuit Breaker → Timeout (30s) — no retry (idempotency risk) |
| Read-heavy low-latency | Hedging → Timeout |
| Background job step | Retry (5x linear) → Timeout (60s) |
Do / Don't
| Do | Don't |
|----|-------|
| Always pass CancellationToken to ExecuteAsync | Let the pipeline ignore cancellation |
| Add UseJitter = true to retry strategies | Use fixed delays (thundering herd) |
| Log retries with OnRetry callback | Retry silently — you lose observability |
| Set ShouldHandle to specific exceptions | Handle all exceptions with default predicate in payment flows |
| Place timeout after retry for per-attempt timeout | Nest retry inside retry (doubles the attempt count unexpectedly) |
| Use AddStandardResilienceHandler for new HTTP clients | Hand-roll retry loops around HttpClient |
| Use circuit breaker for downstream service calls | Use circuit breaker for local in-memory operations |
See Also
skills/caching/SKILL.md— HybridCache stampede protection reduces load, complementing circuit breakersskills/error-handling/SKILL.md—BrokenCircuitExceptionshould surface as aResult.Failureat domain boundaries
---
Source: https://github.com/zdanovichnick/dotnet-pilot
Author: zdanovichnick
Discovered via: skillsdirectory.com
Genre: ai-agents
SKILL.md source
---
name: Resilience
description: Polly v8 resilience patterns for .NET — retry, circuit breaker, timeout, hedging, and IHttpClientFactory integration using Microsoft.Extensions.Resilience.
---
# Resilience
Polly v8 resilience patterns for .NET — retry, circuit breaker, timeout, hedging, and IHttpClientFactory integration using Microsoft.Extensions.Resilience.
---
name: resilience
description: Polly v8 resilience patterns for .NET — retry, circuit breaker, timeout, hedging, and IHttpClientFactory integration using Microsoft.Extensions.Resilience.
---
# Resilience Patterns (Polly v8)
Reference for building fault-tolerant .NET services using Polly v8 and `Microsoft.Extensions.Resilience`. Used by `dnp-planner` and `dnp-tdd-developer-hard`.
## Package Reference
```xml
<PackageReference Include="Microsoft.Extensions.Resilience" Version="9.*" />
<!-- For HTTP clients: -->
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.*" />
```
**Breaking change from Polly v7**: the `Policy.Handle<>().Retry()` API is gone. Use `ResiliencePipelineBuilder` exclusively.
## Core Concepts
| Concept | Purpose |
|---------|---------|
| `ResiliencePipeline` | Executes a delegate through a chain of strategies |
| `ResiliencePipelineBuilder` | Fluent builder — add strategies in order (outermost first) |
| `ResiliencePipelineProvider<TKey>` | DI-resolved registry; resolve pipelines by key |
| `ResilienceContext` | Per-execution metadata (cancellation token, properties) |
Strategies execute in the order they are added. Outer strategies wrap inner ones — add timeout last to apply it per-attempt, or first to apply it to the whole pipeline.
## DI Registration
```csharp
builder.Services.AddResiliencePipeline("database", pipeline =>
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<SqlException>()
.Handle<TimeoutException>(),
OnRetry = args =>
{
// args.Context, args.AttemptNumber, args.Outcome available
Console.WriteLine($"Retry {args.AttemptNumber} after {args.RetryDelay}");
return ValueTask.CompletedTask;
}
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(15)
})
.AddTimeout(TimeSpan.FromSeconds(5)));
```
## Retry Strategy
```csharp
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
// Fixed, Linear, or Exponential
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromSeconds(1), // base delay; doubles each attempt with Exponential
// Adds ±20% random jitter to prevent thundering herd
UseJitter = true,
// Only retry on specific exceptions or results
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.TooManyRequests),
OnRetry = args =>
{
logger.LogWarning(
"Retry attempt {Attempt} after {Delay}ms due to {Exception}",
args.AttemptNumber,
args.RetryDelay.TotalMilliseconds,
args.Outcome.Exception?.Message);
return ValueTask.CompletedTask;
}
})
```
## Circuit Breaker
Opens the circuit when the failure ratio exceeds the threshold, stopping all calls for `BreakDuration`.
```csharp
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
// Open circuit if ≥50% of calls fail
FailureRatio = 0.5,
// Measurement window
SamplingDuration = TimeSpan.FromSeconds(30),
// Minimum calls in window before circuit can open
MinimumThroughput = 5,
// How long circuit stays open before moving to half-open
BreakDuration = TimeSpan.FromSeconds(15),
OnOpened = args =>
{
logger.LogError("Circuit opened for {Duration}s", args.BreakDuration.TotalSeconds);
return ValueTask.CompletedTask;
},
OnClosed = _ =>
{
logger.LogInformation("Circuit closed — service recovered");
return ValueTask.CompletedTask;
}
})
```
When the circuit is open, calls throw `BrokenCircuitException` immediately without hitting the dependency.
## Timeout
```csharp
// Per-attempt timeout (placed after retry — each attempt gets its own timeout)
.AddTimeout(TimeSpan.FromSeconds(5))
// Or with options for callbacks
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(5),
OnTimeout = args =>
{
logger.LogWarning("Operation timed out after {Timeout}", args.Timeout);
return ValueTask.CompletedTask;
}
})
```
## Hedging (Parallel Fallback Requests)
Sends a duplicate request after a delay if the first hasn't returned. Uses the first successful response.
```csharp
.AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
{
// Send a second request after 500ms if first hasn't returned
Delay = TimeSpan.FromMilliseconds(500),
// How many parallel hedged requests to allow
MaxHedgedAttempts = 2,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => !r.IsSuccessStatusCode),
ActionGenerator = args => () =>
ValueTask.FromResult(Outcome.FromResult(
args.PrimaryContext.Properties.GetValue(
new ResiliencePropertyKey<HttpResponseMessage>("hedged-result"),
null!)))
})
```
Use hedging for latency-sensitive read operations where idempotency is guaranteed.
## IHttpClientFactory Integration
### Standard Resilience Handler (recommended shortcut)
Applies retry + circuit breaker + timeout + rate limiter in one call:
```csharp
builder.Services.AddHttpClient("payments", client =>
{
client.BaseAddress = new Uri(builder.Configuration["PaymentsApi:BaseUrl"]!);
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.FailureRatio = 0.5;
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});
```
### Custom Pipeline per Client
```csharp
builder.Services.AddHttpClient("inventory")
.AddResilienceHandler("inventory-pipeline", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 2,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
```
### Consuming the Named Client
```csharp
public class InventoryClient(IHttpClientFactory factory)
{
public async Task<InventoryResponse?> GetStockAsync(string sku, CancellationToken ct)
{
var client = factory.CreateClient("inventory");
var response = await client.GetAsync($"/stock/{sku}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<InventoryResponse>(ct);
}
}
```
## Named Pipeline Usage (Non-HTTP)
```csharp
public class OrderRepository(
AppDbContext db,
ResiliencePipelineProvider<string> pipelines,
ILogger<OrderRepository> logger)
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
{
var pipeline = pipelines.GetPipeline("database");
return await pipeline.ExecuteAsync(
async token => await db.Orders.FindAsync([id], token),
ct);
}
}
```
## Recommended Pipeline Compositions
| Use Case | Strategies (outer → inner) |
|----------|---------------------------|
| Database queries | Retry (3x exponential) → Timeout (5s) |
| HTTP API calls | Retry (3x) → Circuit Breaker → Timeout (10s total) |
| Payment processing | Circuit Breaker → Timeout (30s) — no retry (idempotency risk) |
| Read-heavy low-latency | Hedging → Timeout |
| Background job step | Retry (5x linear) → Timeout (60s) |
## Do / Don't
| Do | Don't |
|----|-------|
| Always pass `CancellationToken` to `ExecuteAsync` | Let the pipeline ignore cancellation |
| Add `UseJitter = true` to retry strategies | Use fixed delays (thundering herd) |
| Log retries with `OnRetry` callback | Retry silently — you lose observability |
| Set `ShouldHandle` to specific exceptions | Handle all exceptions with default predicate in payment flows |
| Place timeout after retry for per-attempt timeout | Nest retry inside retry (doubles the attempt count unexpectedly) |
| Use `AddStandardResilienceHandler` for new HTTP clients | Hand-roll retry loops around `HttpClient` |
| Use circuit breaker for downstream service calls | Use circuit breaker for local in-memory operations |
## See Also
- `skills/caching/SKILL.md` — HybridCache stampede protection reduces load, complementing circuit breakers
- `skills/error-handling/SKILL.md` — `BrokenCircuitException` should surface as a `Result.Failure` at domain boundaries
---
**Source**: https://github.com/zdanovichnick/dotnet-pilot
**Author**: zdanovichnick
**Discovered via**: skillsdirectory.com
**Genre**: ai-agents
Related skills 6
running-claude-code-via-litellm-copilot
Use when routing Claude Code through a local LiteLLM proxy to GitHub Copilot, reducing direct Anthropic spend, configuring ANTHROPIC_BASE_URL or ANTHROPIC_MODEL overrides, or troubleshooting Copilot proxy setup failures such as model-not-found, no localhost traffic, or GitHub 401/403 auth errors.
skills-cli
Use when users ask to discover, install, list, check, update, remove, back up, restore, sync, or initialize Agent Skills, mention `bunx skills`, `npx skills`, `skills.sh`, or `skills-lock.json`, ask "find a skill for X", or want help extending agent capabilities with installable skills.
repo-intake-and-plan
Narrow RigorPilot helper for README-first deep learning repo reproduction. Use when the task is specifically to scan a repository, read the README and common project files, extract documented commands, classify inference, evaluation, and training candidates, and return the smallest trustworthy reproduction plan to the main orchestrator. Do not use for environment setup, asset download, command execution, final reporting, paper lookup, or end-to-end orchestration.
image-to-video
Animate any still image on RunComfy — this skill is a smart router that matches the user's intent to the right i2v model in the RunComfy catalog. Picks HappyHorse 1.0 I2V (Arena #1, native audio, identity preservation) for general animations, Wan 2.7 with `audio_url` for custom-voiceover lip-sync, or Seedance 2.0 Pro for multi-modal animation from image + reference video + reference audio. Bundles each model's documented prompting patterns so the caller gets sharper output without burning ite...
video-edit
Edit existing video on RunComfy — this skill is a smart router that matches the user's intent to the right edit model in the RunComfy catalog. Picks Wan 2.7 Edit-Video (general restyle / background swap / packaging swap, identity + motion preservation), Kling 2.6 Pro Motion Control (transfer precise motion from a reference video to a target character), or Lucy Edit Restyle (lightweight identity-stable restyle / outfit swap). Bundles each model's documented prompting patterns so the skill gets...
nano-banana-2
Generate images with Google Nano Banana 2 (Gemini-family flash-tier text-to-image) on RunComfy — bundled with the model's documented prompting patterns so the skill gets sharper output than naive prompting against the same model. Documents Nano Banana 2's strengths (rapid iteration, in-image typography rendering, predictable framing, optional web-grounded context), the resolution-tier pricing, the safety-tolerance dial, and when to route to Nano Banana Pro / GPT Image 2 / Flux 2 / Seedream in...