NResilience
Prevent cascading failures in your .NET applications.
A struggling dependency can hang your requests, tie up your threads, and crash your application. Blind retries make it worse by piling onto the failing service. NResilience wraps your calls in retries, deadlines, attempt timeouts, and circuit breakers so your application degrades gracefully instead of crashing.
Why NResilience?
NResilience replaces fluent builders, strategy ordering, and mandatory Build() calls with values and C# with expressions.
- No fluent builders. Configure policies with
withexpressions: change one setting, keep the rest. - Sensible defaults. A working, retried HTTP call in one line of code.
- One execution method.
RunAsyncworks for HTTP calls, database queries, or queue reads. - Measured, not guessed. Attempt ceilings, circuit breaker trips, and even the concurrency limit are measured from what the dependency actually does, so you never guess a millisecond figure per dependency.
- It knows when the problem is local. Saturation awareness stops those measurements learning from this process's own thread-pool queue - the one incident where every measured bound loosens at once while the dependency is fine.
- Retry budget. Caps retries as a fraction of traffic, on by default, so a fleet of clients cannot overwhelm a struggling dependency.
- It reads the quota the dependency publishes. Most large APIs send how much allowance is left on every response. The HTTP handler reads it, keeps it per host, and refuses an attempt locally before the 429 - no rate to guess, because the dependency supplies it.
- Hedging. When a call is slow, a duplicate request races it. Hedges pause while the dependency degrades, so they never pile onto a struggling service.
- Deadline propagation. Deadlines travel across services: the gRPC interceptor sends
grpc-timeout, and the ASP.NET middleware reads what a caller sent so outbound calls inherit it. - Criticality propagation. How much a request matters travels with it, so a backfill stops spending the retry capacity a checkout needs and is never hedged. The library carries the level and holds back amplification; what to shed stays your decision.
- Drain-aware shutdown. When the host starts shutting down, calls stop retrying into the rollout and deadlines clamp to what is left of the grace period. On by default for registered policies, because a retry sent after the load balancer stopped routing here is one nobody will read the answer to.
- Testable. Scripted callbacks, a recording listener, and fault injection make policies deterministic in tests.
- Simulate before you ship. Run your policy against a modeled brownout on a virtual clock and read the load multiplier, the availability, and the p99 it produces. Five simulated minutes cost about as much as a unit test.
- It explains itself. One call prints the worst-case timeline attempt by attempt and reports which measured terms are warm, so "how long can this call take?" has an answer you can read.
- Telemetry. Every call raises one event carrying its verdict, retries, and delays; meters and an activity source expose them.
- Production-ready. Built-in analyzers catch common mistakes, such as passing the wrong cancellation token.
- Native AOT compatible. Zero external dependencies and no reflection.
Get started
Add NResilience to your project:
dotnet add package NResilienceFor most HTTP scenarios, use the pre-configured client:
// Create one client for the application's lifetime
private static readonly HttpClient Client = HttpResilience.CreateClient();
private static async Task<User?> GetUserAsync(int id, CancellationToken cancellationToken) =>
await Client.GetFromJsonAsync<User>(new Uri($"https://api.example.com/users/{id}"), cancellationToken);Every call this client makes uses three attempts with exponential backoff, a 30-second deadline, and HTTP-aware retry logic (for example, it retries a 503 but not a 404).
One method for any callback
// 1. Start from a preset. `Resilience.Http` retries and times out an HTTP call out of the box.
var api = Resilience.Http;
// 2. Change one setting, keep the rest: `with` copies everything you did not mention.
var slow = Resilience.Http with { Attempts = 5, Deadline = TimeSpan.FromSeconds(value: 20) };
// 3. Run any callback through one method. The token handed to your work is the attempt's own.
var user = await api.RunAsync(attempt => client.GetFromJsonAsync<User>(requestUri: url, cancellationToken: attempt),
cancellationToken: cancellationToken);
var response = await api.RunAsync(attempt => client.GetAsync(requestUri: url, cancellationToken: attempt), cancellationToken: cancellationToken);
await slow.RunAsync(attempt => queue.FlushAsync(cancellationToken: attempt), cancellationToken: cancellationToken);
// 4. Want the outcome without an exception? `TryRunAsync` hands it back to branch on.
var result = await api.TryRunAsync(attempt => FetchAsync(cancellationToken: attempt), cancellationToken: cancellationToken);
var best = result.TryGetValue(value: out var fetched) ? fetched : cache.LastKnownGood;The attempt token is cancelled when the specific attempt hits its timeout, while the cancellationToken cancels the entire operation.
Handle failures without exceptions
Use TryRunAsync to branch on the outcome instead of catching exceptions:
CallResult<User> result = await api.TryRunAsync(attempt => FetchAsync(attempt), cancellationToken);
User best = result.TryGetValue(out User? fetched) ? fetched : cache.LastKnownGood;Performance and correctness
Built for high-performance .NET applications:
- Low overhead. One flat execution path, so cost does not grow as you add policy settings.
- Built-in analyzers. Seven diagnostics ship with the package to prevent silent failures.
- Native AOT. Works with
net8.0andnet10.0trimming and AOT publishing.
Start here
| If you want | Go to |
|---|---|
| A retried HTTP call in two minutes | Quick start |
| The core terminology | Key concepts |
| Worked scenarios for common patterns | Guides |
| Detailed configuration options | Features |
AddResilience() on a client | Dependency injection |
| A retried gRPC client | gRPC |
| Every member, in order | Reference |
| Architecture and design decisions | Deep dives |
| To move off Polly | Migrating from Polly |
Overhead is one allocation per call, gated in CI. For details, see Where the allocations are.
