FAQ
Design decisions
Why is there no builder?
A policy is a record, so with expressions serve as the configuration language. A builder would provide a validation hook at Build() but would require a mutable-to-immutable transition, introduce an ordering dependency, and prevent the use of static readonly fields.
Validation occurs when you call Validate(), eagerly during dependency injection registration, or lazily on the first execution of each policy instance.
Why is Resilience not generic?
The result type is a property of the call, not the policy. A single policy can handle HttpResponseMessage, int, Stream, or void. Result classification is resolved per result type and cached, so the policy does not need to be generic.
Why is Attempts the total count rather than the retry count?
Using total attempts removes ambiguity. Attempts = 1 means no retry occurs, eliminating off-by-one errors common in retry count configurations.
Is there a synchronous API?
No. A retry loop that blocks holds a thread through every backoff delay. Offering both synchronous and asynchronous APIs would either duplicate the engine or risk deadlocks. For this reason, ResilienceHandler.Send throws a NotSupportedException.
Where is hedging?
Hedging is not implemented. Issuing a second request before the first fails is a dangerous default because it multiplies load on a dependency exactly when it is slow. Implementing hedging safely requires a budget, an adaptive latency threshold, and a per-request idempotency strategy. The retry budget and the circuit breaker provide the necessary groundwork for this feature.
Where is a rate limiter?
NResilience.Extensions provides one, and it does not reimplement System.Threading.RateLimiting - it gives the platform's limiters a correct place to stand. See Rate limiting.
What the library adds is the composition the platform cannot decide for you: the permit is taken once per attempt rather than once per operation, the wait is bounded by the time left on the deadline, and a refusal is classified as self-imposed throttling - so it takes the long backoff curve, never counts as evidence against the dependency, and is never charged to the retry budget. For the reasoning, see Admission control.
Where is bulkhead isolation?
Limit.Concurrency is the bulkhead: it bounds how many calls run against one dependency at once, per host by default. See Resource isolation with bulkheads for a complete guide.
using var limiter = Limit.Concurrency(10); // at most 10 concurrent calls
var result = await policy.RunAsync(async ct =>
{
using var lease = await limiter.AcquireAsync(ct);
return await dependency.CallAsync(ct);
}, cancellationToken);This approach is correct for several reasons:
- Zero allocation when unused - no limiter object means no overhead
- Per-attempt permits - each retry acquires its own permit, so retries don't reuse the same slot
- Deadline-aware - the acquire respects the remaining time on the deadline; no separate timeout to configure
- Correct verdict - refusals are classified as
Verdict.Throttled(SelfImposed: true):- Retried on the long backoff curve (1 second base, not 100 ms) to defend the dependency
- Never opens the circuit breaker (this is your own throttling, not evidence the dependency is broken)
- Never charged to the retry budget (the call never left this process; no amplification)
For HTTP via dependency injection, the handler scopes limiters per host automatically:
services.AddHttpClient("api")
.AddResilience()
.AddRateLimit(options => options.Concurrency = 10); // Per-hostThe callback-based approach respects NResilience's design philosophy: explicit insertion point, zero cost when unused, and integration with the verdict system. A hand-rolled SemaphoreSlim requires manual handling of the deadline, exception flow on timeout, and outcome classification. The limiter handles these for you.
Can I add my own policy layer?
You cannot add layers through composition because the engine is one flat method. Extension points include the classifier, Backoff.Custom, BeforeAttempt, and OnEvent. This restricted surface ensures long-term API stability.
Is a Breaker thread-safe? Can I share one across policies?
Yes. Sharing a breaker allows you to treat multiple different calls as the same dependency. The breaker is guarded by an uncontended lock. Using with copies the reference to the breaker, not its internal state.
Does the breaker see attempts or whole operations?
The breaker always samples individual attempts. This provides a consistent behavior regardless of how the policy is configured.
Why does refusing a call take 100 milliseconds?
A free rejection inside a polling loop creates a CPU spin, turning a load-shedding guard into a load generator. For more details, see Guarded rejection.
Why is telemetry off for hand-built policies but on for registered ones?
Setting OnEvent = null ensures that telemetry is "free when unused." Since policies registered via dependency injection are typically used in production environments, the registration automatically attaches a listener. You can disable this using telemetry: false or ResilienceOptions.Telemetry = false. Logging works the same way and for the same reason: a registered policy logs, a hand-built one opts in with WithLogging, and ResilienceOptions.Logging = "Off" turns it off. See Logging.
Compatibility and performance
Is it AOT and trimming safe?
Yes. Both ahead-of-time (AOT) compilation and trimming are enforced in CI. The build process runs dotnet publish -p:PublishAot=true with warnings treated as errors and verifies that the resulting binary executes a policy - including dependency injection, configuration binding, and the meter - while respecting allocation budgets. The core contains no reflection.
Which frameworks are supported?
NResilience supports net8.0 and net10.0. Both frameworks are tested and gated; specifically, the library ensures there is no "allocation cliff" on net8.0.
Does it work with IHttpClientFactory?
Yes. You can use .AddResilience() on the client builder. For more information about the two-minute handler rotation for configuration reloads, see Dependency injection.
