skills/resilience/retry-patterns/SKILL.md
Use when implementing retry logic with exponential backoff and jitter.
npx skillsauth add faysilalshareef/dotnet-ai-kit retry-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
builder.Services.AddHttpClient("OrdersApi", client =>
{
client.BaseAddress = new Uri("https://api.orders.{Company}.com");
})
.AddResilienceHandler("retry-pipeline", pipeline =>
{
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true, // Adds randomness to prevent thundering herd
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r =>
r.StatusCode == HttpStatusCode.TooManyRequests ||
r.StatusCode == HttpStatusCode.ServiceUnavailable ||
r.StatusCode >= HttpStatusCode.InternalServerError)
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>()
});
});
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
DelayGenerator = args =>
{
// Respect Retry-After header from server
if (args.Outcome.Result?.Headers.RetryAfter is { } retryAfter)
{
var delay = retryAfter.Delta
?? (retryAfter.Date.HasValue
? retryAfter.Date.Value - DateTimeOffset.UtcNow
: TimeSpan.FromSeconds(1));
return ValueTask.FromResult<TimeSpan?>(delay);
}
// Default exponential backoff
return ValueTask.FromResult<TimeSpan?>(
TimeSpan.FromSeconds(Math.Pow(2, args.AttemptNumber)));
},
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r =>
r.StatusCode == HttpStatusCode.TooManyRequests ||
r.StatusCode >= HttpStatusCode.InternalServerError)
});
// EF Core built-in retry
options.UseSqlServer(connectionString, sql =>
{
sql.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null); // null = retry all transient errors
});
// Custom retry with Polly for non-EF operations
builder.Services.AddResiliencePipeline("db-operation", pipeline =>
{
pipeline.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<SqlException>(ex => ex.IsTransient)
.Handle<TimeoutException>()
});
});
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
var logger = args.Context.Properties.GetValue(
new ResiliencePropertyKey<ILogger>("logger"), null!);
logger?.LogWarning(
"Retry attempt {AttemptNumber} after {Delay}ms. " +
"Reason: {Reason}",
args.AttemptNumber,
args.RetryDelay.TotalMilliseconds,
args.Outcome.Exception?.Message ??
args.Outcome.Result?.StatusCode.ToString());
return ValueTask.CompletedTask;
}
});
builder.Services.AddResiliencePipeline("message-queue", pipeline =>
{
pipeline.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
MaxDelay = TimeSpan.FromSeconds(30), // cap max delay
ShouldHandle = new PredicateBuilder()
.Handle<MessageQueueException>()
.Handle<TimeoutException>()
});
});
// Usage
public sealed class MessagePublisher(
[FromKeyedServices("message-queue")] ResiliencePipeline pipeline,
IMessageBus messageBus)
{
public async Task PublishAsync<T>(T message, CancellationToken ct)
{
await pipeline.ExecuteAsync(
async token => await messageBus.SendAsync(message, token),
ct);
}
}
// For POST/PUT operations that create resources
public sealed class PaymentClient(HttpClient httpClient)
{
public async Task<PaymentResult> ChargeAsync(
ChargeRequest request, CancellationToken ct)
{
// Include idempotency key so retries don't duplicate charges
var idempotencyKey = Guid.NewGuid().ToString();
var httpRequest = new HttpRequestMessage(
HttpMethod.Post, "/charges")
{
Content = JsonContent.Create(request)
};
httpRequest.Headers.Add(
"Idempotency-Key", idempotencyKey);
var response = await httpClient.SendAsync(httpRequest, ct);
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<PaymentResult>(ct)
?? throw new InvalidOperationException(
"Empty response from payment service");
}
}
Constant: [1s] [1s] [1s] [1s]
Linear: [1s] [2s] [3s] [4s]
Exponential: [1s] [2s] [4s] [8s]
Exp + Jitter: [0.8s] [2.3s] [3.7s] [8.1s] ← Recommended
AddRetry or RetryStrategyOptions usageWaitAndRetryAsync (migration candidate)EnableRetryOnFailure in EF Core configurationwhile / for with try-catch)Idempotency-Key header usage| Scenario | Max Retries | Base Delay | Backoff | |----------|-------------|-----------|---------| | HTTP API call | 3 | 500ms | Exponential + jitter | | Database connection | 3 | 200ms | Exponential + jitter | | Message queue publish | 5 | 1s | Exponential + jitter | | File upload | 2 | 1s | Exponential | | Payment processing | 1-2 | 1s | Exponential (with idempotency key) |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.