Your dependency will fail - be ready for it
Stop cascading failures in your .NET services. Learn how NResilience wraps HTTP calls in adaptive retries and deadlines to ensure your app degrades gracefully instead of crashing.
TL;DR: A struggling dependency can hang your requests, tie up your thread pool, and take your entire application down. NResilience wraps these calls in retries, deadlines, and time bounds so your app degrades gracefully instead of crashing.
The anatomy of a cascading failure
You have a service that calls another service. Everything works fine until the downstream dependency starts to struggle.
It doesn't crash - it just gets slow.
Your requests start taking 10 seconds instead of 100ms. Each in-flight call holds a thread and a connection. Your request queue fills up. Your memory usage climbs. Suddenly, your "healthy" service is unresponsive because every request is waiting on a dependency that never answers.
This is a cascading failure. One slow service in your stack creates a ripple effect that kills everything upstream.
The naive retry trap
The first instinct is often to add a retry loop.
// Don't do this
for (int i = 0; i < 3; i++)
{
try { return await client.GetAsync(url); }
catch { /* retry */ }
}
But naive retries usually make the problem worse. If the downstream service is struggling because it's overloaded, sending three times the traffic exactly when it's most vulnerable finishes the job the outage started.
If you retry a 404 Not Found, you're wasting resources on a request that will never succeed. If you retry immediately without a delay, you contribute to a "thundering herd" that keeps the dependency pinned at 100% CPU.
To survive failure, you need more than a loop. You need a strategy that understands HTTP, respects time, and backs off.
What is NResilience?
NResilience is a small, fast, dependency-free library built around one core idea: state your bounds as plain values and let one flat executor enforce them.
It exists to solve the common pitfalls of .NET resilience. Instead of building a complex pipeline, you get four key advantages:
- Policies are immutable values: You derive variants using the
withkeyword, not a builder you have to get the order right on. - Measured bounds: Your timeouts can be measured from real dependency latency instead of guessed at.
- One flat executor: You get a single, low-allocation execution path instead of a layered chain of strategy objects.
- Built-in safeguards: The mistakes that usually fail silently - like the wrong cancellation token - are caught by analyzers that ship in the box.
It supports Native AOT and has zero external dependencies, targeting net8.0 and net10.0.
Resilience in one line
You can implement this today without re-architecting your app. NResilience ships a pre-configured HTTP integration.
You can create a resilient HttpClient in a single call:
using System.Net.Http.Json;
using NResilience;
// One client for the application's lifetime, with the policy already inside it.
private static readonly HttpClient Client = HttpResilience.CreateClient();
// This call is now wrapped in retries, deadlines, and adaptive attempt timeouts
var data = await Client.GetFromJsonAsync<WeatherForecast>(
new Uri("https://api.example.com/weather"), cancellationToken);
What the defaults actually buy you
HttpResilience.CreateClient() comes with a set of defaults designed for distributed systems.
1. HTTP-aware classification
Not every failure is worth retrying. NResilience uses a Classifier to decide if a result is worth retrying.
- 503 Service Unavailable is treated as transient. It retries.
- 429 Too Many Requests is throttling. It retries with a longer pause, honoring
Retry-Afterwhen the server sends one. - 404 Not Found is an answer. It does not retry.
You stop wasting cycles on requests that are destined to fail.
2. Exponential backoff with jitter
Instead of hammering the server, NResilience waits between attempts. The wait grows with each failure.
It also adds jitter - a random variation in the delay. This prevents a fleet of a thousand clients from all retrying at the exact same millisecond, which would otherwise create spikes of traffic that crash the dependency again.
3. The total deadline
A common bug in .NET resilience is the "30s x 3" problem: you have a 30-second timeout per attempt and 3 retries, meaning a failing call can hang your thread for 90 seconds.
NResilience uses a Deadline. The default is 30 seconds for the entire operation. If the first attempt takes 20 seconds and fails, the second attempt only has 10 seconds left before the whole call is cancelled. Getting an answer out of the dependency never takes longer than the deadline you specified.
Reading that answer is a separate question, and it is one most libraries get wrong. An HTTP attempt ends when the response headers arrive - the body is a live stream you read afterwards, outside everything the deadline covers. NResilience bounds it with the number you already set: a body that stops arriving for longer than the AttemptTimeout fails your read instead of hanging on it. What is bounded is the gap between two reads rather than the total, so a large download that keeps arriving is never cut off for taking a while. Nothing waits forever; not everything finishes inside the deadline.
4. The attempt ceiling
Beyond the total deadline, there is a per-attempt bound. It starts at 10 seconds, and it usually tightens itself: the default ceiling is three times what a call to that host recently took, measured per host, so a dependency whose p95 is 40 ms gets a ~120 ms ceiling. If a single request exceeds the ceiling, it is cancelled, and the library moves to the next retry. This prevents a single "zombie" request from consuming the majority of your total deadline.
Looking ahead
HttpResilience.CreateClient() is the fastest way to get started, but it's just one application of a policy.
In NResilience, a policy is a plain C# value. You can derive a specialized version of the HTTP policy using the with keyword to change the deadline or the number of attempts without needing a complex builder. You can also use RunAsync to wrap any arbitrary piece of code - database calls, file I/O, or third-party SDKs - in the same resilience logic.
But before you customize the policy, you need to understand why we treat policies as values rather than pipelines.
Policies are values, not pipelines explains the record-based model and how with expressions replace the builder pattern.