Timeout and Policy Composition
Timeouts prevent operations from hanging indefinitely. Combining multiple policies creates comprehensive resilience strategies.
Timeout Policies
Optimistic Timeout
Relies on the operation supporting cancellation:
csharp1var timeout = Policy2 .TimeoutAsync(TimeSpan.FromSeconds(10));34await timeout.ExecuteAsync(async ct =>5{6 // ct is a CancellationToken - pass it to operations7 return await httpClient.GetAsync(url, ct);8}, CancellationToken.None);
Pessimistic Timeout
Forces timeout even if operation doesn't support cancellation:
csharp1var timeout = Policy2 .TimeoutAsync(3 TimeSpan.FromSeconds(10),4 TimeoutStrategy.Pessimistic);56// Warning: the underlying operation continues running!
Timeout with Callback
csharp1var timeout = Policy2 .TimeoutAsync(3 TimeSpan.FromSeconds(10),4 onTimeoutAsync: (context, timeSpan, task) =>5 {6 Console.WriteLine($"Timeout after {timeSpan.TotalSeconds}s");7 return Task.CompletedTask;8 });
Combining Policies with Wrap
csharp1// Individual policies2var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(10));34var retry = Policy5 .Handle<HttpRequestException>()6 .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1));78var breaker = Policy9 .Handle<HttpRequestException>()10 .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));1112// Combine them13var combined = Policy.WrapAsync(timeout, breaker, retry);1415// Execution order: timeout → breaker → retry → operation16await combined.ExecuteAsync(() => httpClient.GetAsync(url));
Policy Execution Order
1timeout.Execute(2 breaker.Execute(3 retry.Execute(4 operation()5 )6 )7)
Order matters!
- Timeout should wrap everything (fail if total time exceeded)
- Circuit breaker wraps retry (each failed retry set counts as one failure)
- Retry is innermost (retries the actual operation)
Timeout Per Retry vs Overall
Timeout Per Retry
csharp1// 10s timeout for EACH attempt2var timeoutPerRetry = Policy.TimeoutAsync(TimeSpan.FromSeconds(10));3var retry = Policy4 .Handle<HttpRequestException>()5 .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1));67// Retry wraps timeout8var policy = Policy.WrapAsync(retry, timeoutPerRetry);910// Total max time: (10s + 1s) * 3 = 33s
Overall Timeout
csharp1var overallTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(30));2var retry = Policy3 .Handle<HttpRequestException>()4 .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1));56// Timeout wraps retry7var policy = Policy.WrapAsync(overallTimeout, retry);89// Total max time: 30s (retries stop if overall timeout hit)
Both
csharp1var overallTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(30));2var perRetryTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(10));3var retry = Policy4 .Handle<HttpRequestException>()5 .Or<TimeoutRejectedException>() // Handle timeout as retryable6 .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1));78// Overall wraps retry wraps per-retry timeout9var policy = Policy.WrapAsync(overallTimeout, retry, perRetryTimeout);
Fallback Policy
Provide a default when all else fails:
csharp1var fallback = Policy<string>2 .Handle<Exception>()3 .FallbackAsync(4 fallbackValue: "Default data",5 onFallbackAsync: (result, context) =>6 {7 Console.WriteLine($"Using fallback: {result.Exception?.Message}");8 return Task.CompletedTask;9 });1011var retry = Policy<string>12 .Handle<Exception>()13 .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1));1415var policy = Policy.WrapAsync(fallback, retry);1617// Returns "Default data" if all retries fail18string result = await policy.ExecuteAsync(() => FetchDataAsync());
Complete Resilience Stack
csharp1class ResilientClient2{3 private readonly HttpClient _client;4 private readonly IAsyncPolicy<HttpResponseMessage> _policy;56 public ResilientClient()7 {8 _client = new HttpClient();910 // 1. Overall timeout (30 seconds max)11 var overallTimeout = Policy12 .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(30));1314 // 2. Circuit breaker15 var breaker = Policy<HttpResponseMessage>16 .Handle<HttpRequestException>()17 .OrResult(r => (int)r.StatusCode >= 500)18 .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));1920 // 3. Retry with exponential backoff21 var retry = Policy<HttpResponseMessage>22 .Handle<HttpRequestException>()23 .Or<TimeoutRejectedException>()24 .OrResult(r => (int)r.StatusCode >= 500)25 .WaitAndRetryAsync(3, attempt =>26 TimeSpan.FromSeconds(Math.Pow(2, attempt)));2728 // 4. Per-request timeout (10 seconds per attempt)29 var perRequestTimeout = Policy30 .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));3132 // Combine: overall → breaker → retry → per-request → operation33 _policy = Policy.WrapAsync(34 overallTimeout,35 breaker,36 retry,37 perRequestTimeout);38 }3940 public async Task<string> GetAsync(string url)41 {42 try43 {44 var response = await _policy.ExecuteAsync(45 ct => _client.GetAsync(url, ct),46 CancellationToken.None);4748 response.EnsureSuccessStatusCode();49 return await response.Content.ReadAsStringAsync();50 }51 catch (BrokenCircuitException)52 {53 throw new ServiceUnavailableException("Service is down");54 }55 catch (TimeoutRejectedException)56 {57 throw new ServiceUnavailableException("Request timed out");58 }59 }60}
Policy Registry
Manage policies centrally:
csharp1var registry = new PolicyRegistry2{3 { "StandardRetry", Policy.Handle<Exception>().RetryAsync(3) },4 { "AggressiveRetry", Policy.Handle<Exception>().WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(1)) },5 { "StandardBreaker", Policy.Handle<Exception>().CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)) }6};78// Use by name9var policy = registry.Get<IAsyncPolicy>("StandardRetry");10await policy.ExecuteAsync(() => DoWorkAsync());
Key Takeaways
- Timeout prevents operations from hanging
- Optimistic timeout requires CancellationToken support
- Policy.Wrap combines multiple policies
- Order: Timeout → Breaker → Retry (outermost to innermost)
- Use both overall and per-retry timeouts
- Fallback provides graceful degradation
- Policy registry centralizes policy management