Exponential Backoff
Exponential backoff increases the delay between retries, reducing load on struggling services and improving recovery chances.
Why Exponential Backoff?
Without Backoff
1Attempt 1: 0ms ← fail2Attempt 2: 0ms ← fail3Attempt 3: 0ms ← fail4Attempt 4: 0ms ← fail56Server is hammered with requests while overloaded!
With Exponential Backoff
1Attempt 1: 0ms ← fail2Attempt 2: 1000ms ← fail (waited 1s)3Attempt 3: 2000ms ← fail (waited 2s)4Attempt 4: 4000ms ← success (waited 4s)56Server has time to recover between attempts
Basic Exponential Backoff
csharp1var policy = Policy2 .Handle<HttpRequestException>()3 .WaitAndRetryAsync(4 5, // 5 retries5 retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))6 // Delays: 2s, 4s, 8s, 16s, 32s7 );
The Thundering Herd Problem
When many clients retry at the same time:
1Time 0: 100 requests → service fails2Time 2s: 100 retries → service fails again!3Time 4s: 100 retries → service fails again!
All clients retry in lockstep, overwhelming the recovering service.
Solution: Jitter
Add randomness to spread out retries:
csharp1var random = new Random();23var policy = Policy4 .Handle<HttpRequestException>()5 .WaitAndRetryAsync(6 5,7 retryAttempt =>8 {9 var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));10 var jitter = TimeSpan.FromMilliseconds(random.Next(0, 1000));11 return baseDelay + jitter;12 });
Now retries are spread across a time window:
1Client 1: 2.1s, 4.3s, 8.7s2Client 2: 2.5s, 4.1s, 8.2s3Client 3: 2.3s, 4.8s, 8.5s
Decorrelated Jitter (Recommended)
AWS-style decorrelated jitter provides better distribution:
csharp1static class JitterExtensions2{3 private static readonly Random Random = new();45 public static TimeSpan DecorrelatedJitter(6 int retryAttempt,7 TimeSpan minDelay,8 TimeSpan maxDelay)9 {10 var ceiling = Math.Min(maxDelay.TotalMilliseconds,11 minDelay.TotalMilliseconds * Math.Pow(2, retryAttempt));1213 var delay = Random.NextDouble() * ceiling;14 return TimeSpan.FromMilliseconds(delay);15 }16}1718var policy = Policy19 .Handle<HttpRequestException>()20 .WaitAndRetryAsync(21 5,22 attempt => JitterExtensions.DecorrelatedJitter(23 attempt,24 TimeSpan.FromMilliseconds(100),25 TimeSpan.FromSeconds(30)));
Polly.Contrib.WaitAndRetry
The recommended package for backoff calculations:
bash1dotnet add package Polly.Contrib.WaitAndRetry
csharp1using Polly.Contrib.WaitAndRetry;23// Decorrelated jitter backoff4var delay = Backoff.DecorrelatedJitterBackoffV2(5 medianFirstRetryDelay: TimeSpan.FromSeconds(1),6 retryCount: 5);78var policy = Policy9 .Handle<HttpRequestException>()10 .WaitAndRetryAsync(delay);
Respecting Retry-After Headers
Many APIs return Retry-After headers:
csharp1var policy = Policy<HttpResponseMessage>2 .HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)3 .WaitAndRetryAsync(4 3,5 sleepDurationProvider: (retryCount, response, context) =>6 {7 // Use Retry-After if provided8 var retryAfter = response.Result?.Headers.RetryAfter;910 if (retryAfter?.Delta != null)11 return retryAfter.Delta.Value;1213 if (retryAfter?.Date != null)14 return retryAfter.Date.Value - DateTimeOffset.UtcNow;1516 // Fall back to exponential backoff17 return TimeSpan.FromSeconds(Math.Pow(2, retryCount));18 },19 onRetryAsync: (response, timeSpan, retryCount, context) =>20 {21 Console.WriteLine($"Rate limited, waiting {timeSpan.TotalSeconds}s");22 return Task.CompletedTask;23 });
Maximum Delay Cap
Prevent extremely long waits:
csharp1var policy = Policy2 .Handle<HttpRequestException>()3 .WaitAndRetryAsync(4 10,5 retryAttempt =>6 {7 var exponentialDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));8 var maxDelay = TimeSpan.FromMinutes(1);9 return exponentialDelay < maxDelay ? exponentialDelay : maxDelay;10 });11// Delays: 2s, 4s, 8s, 16s, 32s, 60s, 60s, 60s, 60s, 60s
Complete Example
csharp1class RateLimitedApiClient2{3 private readonly HttpClient _client;4 private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;56 public RateLimitedApiClient()7 {8 _client = new HttpClient();910 var backoff = Backoff.DecorrelatedJitterBackoffV2(11 medianFirstRetryDelay: TimeSpan.FromMilliseconds(500),12 retryCount: 5);1314 _retryPolicy = Policy<HttpResponseMessage>15 .Handle<HttpRequestException>()16 .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)17 .OrResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)18 .WaitAndRetryAsync(19 backoff,20 onRetry: (outcome, timeSpan, retryCount, context) =>21 {22 Console.WriteLine(23 $"[Retry {retryCount}] Waiting {timeSpan.TotalMilliseconds}ms. " +24 $"Status: {outcome.Result?.StatusCode}");25 });26 }2728 public async Task<string> GetDataAsync(string endpoint)29 {30 var response = await _retryPolicy.ExecuteAsync(31 () => _client.GetAsync(endpoint));3233 response.EnsureSuccessStatusCode();34 return await response.Content.ReadAsStringAsync();35 }36}
Key Takeaways
- Exponential backoff: delay doubles each retry
- Jitter prevents thundering herd
- Decorrelated jitter provides best distribution
- Use Polly.Contrib.WaitAndRetry for backoff calculations
- Respect Retry-After headers from APIs
- Cap maximum delay to prevent very long waits
- Combine with circuit breaker (next lesson)