Combining Policies
Policies compose naturally in the resilience pipeline. Understanding the execution order is key to building effective combinations.
Fixed Execution Order
Regardless of the order you add them with the builder, policies always execute in this order:
Fallback → Chaos → RateLimiter → Retry → Timeout → CircuitBreaker → Bulkhead → Hedge → Handler
This means:
- Fallback sees all failures from all other policies
- Chaos and RateLimiter are evaluated once per logical call, not retried — a rate-limit rejection no longer gets retried or counted against the circuit breaker
- Retry fires after RateLimiter lets the call through, and wraps everything inside it (CircuitBreaker, Bulkhead, Hedge)
- Timeout applies per attempt (inside Retry)
- CircuitBreaker sits between Timeout and Bulkhead — an open circuit short-circuits before a bulkhead slot is taken
- Hedge runs the actual operation, potentially in parallel, as the innermost policy
Common Combinations
External API (Read)
var policy = ResiliencePolicy.Create()
.Fallback<ApiResponse>(opts =>
{
opts.FallbackValue = ApiResponse.Empty;
})
.Retry(opts =>
{
opts.MaxRetries = 3;
opts.BackoffType = BackoffType.Exponential;
opts.DelayMs = 200;
})
.CircuitBreaker(opts =>
{
opts.CircuitKey = "external-api";
opts.FailureThreshold = 10;
opts.BreakDuration = TimeSpan.FromSeconds(60);
})
.Timeout(TimeSpan.FromSeconds(10))
.Build();
Database Write
var policy = ResiliencePolicy.Create()
.Retry(opts =>
{
opts.MaxRetries = 2;
opts.BackoffType = BackoffType.Linear;
opts.DelayMs = 100;
// Only retry on transient DB errors
opts.RetryOnExceptions = new[] { typeof(SqlException) };
})
.Timeout(TimeSpan.FromSeconds(30))
.Build();
Low-Latency Read with Hedge
var policy = ResiliencePolicy.Create()
.Fallback<string>(opts => { opts.FallbackValue = string.Empty; })
.Timeout(TimeSpan.FromSeconds(2))
.Hedge(opts =>
{
opts.HedgeDelay = TimeSpan.FromMilliseconds(100); // P95 latency
opts.MaxHedgedAttempts = 1;
})
.Build();
Full Protection
var policy = ResiliencePolicy.Create()
.Fallback<Result<OrderDto>>(opts =>
{
opts.FallbackValue = Result<OrderDto>.Fail(
"Service temporarily unavailable.", ErrorType.Failure);
})
.RateLimiter(opts =>
{
opts.Algorithm = RateLimiterAlgorithm.TokenBucket;
opts.BucketCapacity = 100;
opts.TokensPerInterval = 10;
opts.ReplenishmentInterval = TimeSpan.FromSeconds(1);
})
.Retry(opts =>
{
opts.MaxRetries = 3;
opts.BackoffType = BackoffType.Exponential;
})
.CircuitBreaker(opts =>
{
opts.CircuitKey = "order-service";
opts.FailureThreshold = 5;
opts.BreakDuration = TimeSpan.FromSeconds(30);
})
.Timeout(TimeSpan.FromSeconds(5))
.Build();
ResiliencePresets
Use built-in presets for common scenarios:
// For external API calls
var policy = ResiliencePresets.ForExternalApi();
// For database access
var policy = ResiliencePresets.ForDatabase();
// For critical paths with fallback
var policy = ResiliencePresets.ForCritical(fallbackValue: Result<string>.Fail("Unavailable", ErrorType.Failure));
Tips
Design Principles
- Always add Fallback when the caller cannot handle exceptions
- Retry wraps CircuitBreaker (enforced by execution order) — each retry attempt passes through the circuit breaker, so an open circuit still short-circuits individual attempts instead of letting them all fail slowly
- Timeout applies per retry attempt — a 5s timeout with 3 retries = up to 15s total + backoff
- Hedge reduces P99 latency — not error recovery. Use Retry for errors, Hedge for latency.
- Use CircuitKey to share circuit state across multiple operations targeting the same service