A minimalist, dependency-free retry and timeout utility for C#. Build resilient applications with simple, type-safe retry policies.
Fox.RetryKit provides a clean, lightweight implementation of retry and timeout patterns without any dependencies:
- Zero Dependencies - No external dependencies, not even Microsoft.Extensions.*
- Type-Safe - Fluent API with compile-time safety
- Flexible - Fixed delays, exponential backoff, and custom delay sequences
- Timeout Support - Built-in timeout with cancellation token integration
- Exception Filtering - Retry only specific exception types
- Async-First - Full support for async/await patterns
- Observability - OnRetry callbacks for logging and metrics
- Jitter - Random variation to prevent thundering herd
- Conditional Retry - Retry based on result values (RetryIf)
- Fallback - Graceful degradation with default values
- Telemetry - Detailed retry metrics (ExecuteWithResult)
- Lightweight - Minimal overhead, simple API, fast compilation
- Well-Documented - Comprehensive XML documentation for IntelliSense
dotnet add package Fox.RetryKitusing Fox.RetryKit;
// Retry up to 3 times
var policy = RetryPolicy.Retry(3);
policy.Execute(() =>
{
// Your potentially failing operation
CallUnstableService();
});// Retry 3 times with 500ms delay between attempts
var policy = RetryPolicy.Retry(3, TimeSpan.FromMilliseconds(500));
await policy.ExecuteAsync(async () =>
{
await CallUnstableApiAsync();
});// Retry 5 times with exponential backoff (100ms, 200ms, 400ms, 800ms, 1600ms)
var policy = RetryPolicy.ExponentialBackoff(5, TimeSpan.FromMilliseconds(100));
var result = await policy.ExecuteAsync(async () =>
{
return await FetchDataAsync();
});// Cancel operation if it takes longer than 5 seconds
var policy = RetryPolicy.Timeout(TimeSpan.FromSeconds(5));
await policy.ExecuteAsync(async () =>
{
await LongRunningOperationAsync();
});// Retry only on specific exception types
var policy = RetryPolicy.Retry(3)
.Handle<HttpRequestException>()
.Handle<TimeoutException>();
await policy.ExecuteAsync(async () =>
{
await CallExternalApiAsync();
});// Combine retry, delay, timeout, and exception filtering
var policy = RetryPolicy.Retry(3, TimeSpan.FromMilliseconds(500))
.Handle<HttpRequestException>()
.WithTimeout(TimeSpan.FromSeconds(10));
var data = await policy.ExecuteAsync(async () =>
{
return await FetchFromUnstableServiceAsync();
});Add logging or metrics on each retry attempt:
var policy = RetryPolicy.Retry(3, TimeSpan.FromMilliseconds(100))
.OnRetry((exception, attempt, delay) =>
{
_logger.LogWarning("Retry attempt {Attempt} after {Delay}ms: {Message}",
attempt, delay.TotalMilliseconds, exception.Message);
});
await policy.ExecuteAsync(async () => await CallServiceAsync());Add random variation (±25%) to delays to prevent synchronized retries:
var policy = RetryPolicy.Retry(5, TimeSpan.FromMilliseconds(200))
.WithJitter(); // Delays will vary: 150-250ms randomly
await policy.ExecuteAsync(async () => await CallSharedResourceAsync());Limit exponential backoff growth to prevent excessive wait times:
var policy = RetryPolicy.ExponentialBackoff(10, TimeSpan.FromMilliseconds(100))
.WithMaxDelay(TimeSpan.FromSeconds(5)); // Cap at 5 seconds
await policy.ExecuteAsync(async () => await CallServiceAsync());Define a custom progression of delays:
var delays = new[]
{
TimeSpan.FromMilliseconds(100),
TimeSpan.FromMilliseconds(250),
TimeSpan.FromMilliseconds(500),
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2)
};
var policy = RetryPolicy.WaitAndRetry(delays);
await policy.ExecuteAsync(async () => await CallServiceAsync());Provide a default value if all retries fail:
var policy = RetryPolicy.Retry(3, TimeSpan.FromMilliseconds(100));
// Static fallback value
var result = policy.Fallback(() => GetRemoteData(), "default-value");
// Dynamic fallback provider
var result = policy.Fallback(
() => GetPrimaryData(),
() => GetCachedData()
);
// Async versions
var result = await policy.FallbackAsync(
async () => await GetRemoteDataAsync(),
"default-value"
);Retry based on the result value, not just exceptions:
var policy = RetryPolicy.Retry(5, TimeSpan.FromMilliseconds(100))
.RetryIf<HttpResponseMessage>(response => !response.IsSuccessStatusCode);
var response = await policy.ExecuteAsync(async () =>
{
return await httpClient.GetAsync(url);
});Get detailed metrics about retry operations:
var policy = RetryPolicy.Retry(3, TimeSpan.FromMilliseconds(150));
var result = await policy.ExecuteWithResultAsync(async () =>
{
return await FetchDataAsync();
});
Console.WriteLine($"Success: {result.Success}");
Console.WriteLine($"Value: {result.Value}");
Console.WriteLine($"Attempts: {result.Attempts}");
Console.WriteLine($"Duration: {result.TotalDuration}");
Console.WriteLine($"Last Exception: {result.LastException?.Message}");var httpPolicy = RetryPolicy.Retry(5, TimeSpan.FromMilliseconds(500))
.Handle<HttpRequestException>()
.WithTimeout(TimeSpan.FromSeconds(30));
var response = await httpPolicy.ExecuteAsync(async () =>
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com/data");
});var dbPolicy = RetryPolicy.ExponentialBackoff(3, TimeSpan.FromSeconds(1))
.Handle<SqlException>()
.Handle<TimeoutException>();
await dbPolicy.ExecuteAsync(async () =>
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
// Execute queries
});var filePolicy = RetryPolicy.Retry(3, TimeSpan.FromMilliseconds(100))
.Handle<IOException>();
var content = filePolicy.Execute(() =>
{
return File.ReadAllText("config.json");
});Fox.RetryKit is built around a simple, composable RetryPolicy class:
RetryPolicy
├── Retry(count) → Immediate retry
├── Retry(count, delay) → Fixed delay retry
├── ExponentialBackoff(count, delay) → Exponential backoff retry
└── Timeout(duration) → Timeout constraint
Fluent Extensions
├── Handle<TException>() → Exception filtering
└── WithTimeout(duration) → Add timeout to retry policy
Execution
├── Execute(action) → Synchronous execution
├── Execute<T>(func) → Synchronous with result
├── ExecuteAsync(func) → Asynchronous execution
└── ExecuteAsync<T>(func) → Asynchronous with result
// Create retry policy with count
RetryPolicy.Retry(int count)
// Create retry policy with count and fixed delay
RetryPolicy.Retry(int count, TimeSpan delay)
// Create exponential backoff policy
RetryPolicy.ExponentialBackoff(int retries, TimeSpan initialDelay)
// Create timeout policy
RetryPolicy.Timeout(TimeSpan duration)
// Create custom delay sequence policy
RetryPolicy.WaitAndRetry(IEnumerable<TimeSpan> delays)// Filter exceptions to retry
policy.Handle<TException>()
// Add timeout constraint
policy.WithTimeout(TimeSpan duration)
// Add retry callback for observability
policy.OnRetry(Action<Exception, int, TimeSpan> callback)
// Enable jitter (±25% random variation)
policy.WithJitter()
// Cap maximum delay (for exponential backoff)
policy.WithMaxDelay(TimeSpan maxDelay)
// Conditional retry based on result
policy.RetryIf<T>(Func<T, bool> predicate)
// Execute synchronously
policy.Execute(Action action, CancellationToken token = default)
policy.Execute<T>(Func<T> func, CancellationToken token = default)
// Execute asynchronously
policy.ExecuteAsync(Func<Task> func, CancellationToken token = default)
policy.ExecuteAsync<T>(Func<Task<T>> func, CancellationToken token = default)
// Execute with telemetry
policy.ExecuteWithResult(Action action, CancellationToken token = default)
policy.ExecuteWithResult<T>(Func<T> func, CancellationToken token = default)
policy.ExecuteWithResultAsync(Func<Task> func, CancellationToken token = default)
policy.ExecuteWithResultAsync<T>(Func<Task<T>> func, CancellationToken token = default)
// Fallback on failure
policy.Fallback<T>(Func<T> func, T fallbackValue, CancellationToken token = default)
policy.Fallback<T>(Func<T> func, Func<T> fallbackProvider, CancellationToken token = default)
policy.FallbackAsync<T>(Func<Task<T>> func, T fallbackValue, CancellationToken token = default)
policy.FallbackAsync<T>(Func<Task<T>> func, Func<Task<T>> fallbackProvider, CancellationToken token = default)Fox.RetryKit is intentionally lightweight and feature-focused. The goal is to remain a simple, zero-dependency library for retry and timeout patterns.
- ✅ Bug fixes - Issues with existing functionality
- ✅ Documentation improvements - Clarifications, examples, typo fixes
- ✅ Performance optimizations - Without breaking API compatibility
- ❌ New dependencies or third-party packages
- ❌ Large feature additions that increase complexity
- ❌ Breaking API changes
If you want to propose a significant change, please open an issue first to discuss whether it aligns with the project's philosophy.
The project enforces a strict build policy to ensure code quality:
- ❌ No errors allowed - Build must be error-free
- ❌ No warnings allowed - All compiler warnings must be resolved
- ❌ No messages allowed - Informational messages must be suppressed or addressed
All pull requests must pass this requirement.
Fox.RetryKit follows strict coding standards:
- Comprehensive unit tests required (xUnit + FluentAssertions)
- Maximum test coverage required - Aim for 100% line and branch coverage. Tests may only be omitted if they would introduce artificial complexity (e.g., testing unreachable code paths, framework internals, or compiler-generated code). Use
[ExcludeFromCodeCoverage]sparingly and only for justified cases. - XML documentation for all public APIs - Clear, concise documentation with examples
- Follow Microsoft coding conventions - See
.github/copilot-instructions.mdfor project-specific style - Zero warnings, zero errors build policy - Strict enforcement
- Follow the existing code style (see
.github/copilot-instructions.md) - Use file-scoped namespaces
- Enable nullable reference types
- Use expression-bodied members for simple properties/methods
- Private fields: camelCase without underscore prefix
- Add XML documentation decorators (98-character width)
- Fork the repository
- Create a feature branch from
main - Follow the coding standards in
.github/copilot-instructions.md - Write comprehensive unit tests (aim for 100% coverage)
- Ensure all tests pass and build is clean (zero warnings/errors)
- Submit a pull request
For detailed guidelines, see CONTRIBUTING.md.
This project is licensed under the MIT License - see the LICENSE.txt file for details.
Fox.RetryKit is part of the Fox.*Kit family of minimal utility libraries for .NET.