ZeroAlloc.Resilience
3.3.0
dotnet add package ZeroAlloc.Resilience --version 3.3.0
NuGet\Install-Package ZeroAlloc.Resilience -Version 3.3.0
<PackageReference Include="ZeroAlloc.Resilience" Version="3.3.0" />
<PackageVersion Include="ZeroAlloc.Resilience" Version="3.3.0" />
<PackageReference Include="ZeroAlloc.Resilience" />
paket add ZeroAlloc.Resilience --version 3.3.0
#r "nuget: ZeroAlloc.Resilience, 3.3.0"
#:package ZeroAlloc.Resilience@3.3.0
#addin nuget:?package=ZeroAlloc.Resilience&version=3.3.0
#tool nuget:?package=ZeroAlloc.Resilience&version=3.3.0
ZeroAlloc.Resilience
Source-generated, zero-allocation resilience policies for .NET.
Add [Retry], [Timeout], [RateLimit], and [CircuitBreaker] to an interface. A Roslyn source generator emits a proxy class that composes all policies in declaration order with no heap allocation on the happy path (beyond the unavoidable CancellationTokenSource for timeout). AOT-safe.
Quick start
dotnet add package ZeroAlloc.Resilience
[Retry(MaxAttempts = 3, BackoffMs = 200, Jitter = true, PerAttemptTimeoutMs = 1_000)]
[Timeout(Ms = 5_000)]
[RateLimit(MaxPerSecond = 100, BurstSize = 10)]
[CircuitBreaker(MaxFailures = 5, ResetMs = 1_000, HalfOpenProbes = 1, Fallback = nameof(FetchFallback))]
public interface IExternalService
{
ValueTask<string> FetchAsync(string id, CancellationToken ct);
ValueTask<string> FetchFallback(string id, CancellationToken ct);
}
// Register — one line wires everything
builder.Services.AddExternalServiceResilience<ExternalServiceImpl>();
Inject IExternalService anywhere — all policies are transparent to the caller.
Performance
Head-to-head vs Polly v8 (the de-facto resilience library in .NET). .NET 10.0.7, BenchmarkDotNet v0.14.0.
| Operation | Polly v8 | ZA.Resilience | Speedup |
|---|---|---|---|
| Retry, happy path | 600 ns / 64 B | 23 ns / 0 B | 26× faster, 0 B alloc |
| CircuitBreaker, closed | 776 ns / 64 B | 17 ns / 0 B | 45× faster, 0 B alloc |
| Retry with 2/3 failures | 22.9 ms / 3,134 B | 27.9 ms / 948 B | 22% slower wall-clock, 3.3× less alloc |
The happy-path gap is driven by Polly's ResiliencePipeline.ExecuteAsync walking the strategy chain via delegate dispatch and allocating a ResilienceContext per call (64 B). ZA emits one direct method per interface — retry/CB checks are inline if statements with no context object or closure.
The retry-with-failures wall-clock gap is dominated by Task.Delay(BackoffMs); the residual 22% is ZA's for-loop scheduling — measurable, but mostly invisible against I/O latency.
Full methodology + self-benchmark: docs/performance.md.
Features
| Feature | Notes |
|---|---|
| Zero allocation on happy path | Policy checks are integer comparisons and CAS operations |
| AOT / trimmer safe | Generated proxy is concrete; no reflection at runtime |
| Retry with exponential backoff | Jitter, per-attempt timeout, total timeout all configurable |
| Timeout | Total operation timeout wrapping all retries and backoff delays |
| Rate limiting | Lock-free token bucket; Shared (singleton) or Instance (per-proxy) scope |
| Circuit breaker | Closed → Open → HalfOpen FSM backed by ZeroAlloc.StateMachine (concurrent CAS) |
| Fallback | Method called when circuit is open — same signature, no allocation |
Result<T> support |
Return Result<T> to get failures without exceptions |
| Result-aware retry | RetryWhen retries the failed Results you call transient; DelayHint honours Retry-After, capped by MaxDelayMs |
| Retry attempt number | [RetryAttempt] on an int? or int parameter passes the retry number to the inner call, for a retry-count header |
| Method-level overrides | Any attribute on a method shadows the interface-level config for that method |
| DI integration | Generated Add{Service}Resilience<TImpl>() extension registers the implementation, a {Service}ResiliencePolicies singleton, and the proxy |
Policy execution order
Policies execute in this order on every call:
RateLimit → CircuitBreaker → Timeout → Retry (with PerAttemptTimeout inside)
Each policy runs before the inner call is even attempted. If the rate limiter rejects, the circuit breaker and retry logic are never reached.
Attribute overview
[Retry]
[Retry(MaxAttempts = 3, BackoffMs = 200, Jitter = true, PerAttemptTimeoutMs = 1_000)]
| Property | Type | Default | Description |
|---|---|---|---|
MaxAttempts |
int |
3 |
Total attempts (initial + retries) |
BackoffMs |
int |
200 |
Base backoff ms; actual = BackoffMs * 2^attempt |
Jitter |
bool |
false |
Add up to 50% random jitter to prevent thundering-herd |
PerAttemptTimeoutMs |
int |
0 |
Per-attempt cancellation timeout; 0 = disabled |
NonThrowing |
bool |
false |
Asserts a ResilienceError Result return type |
RetryWhen |
string? |
null |
Static bool M(E error): retry the failed Results it returns true for; a transient one is a breaker failure, any other a success |
RetryOnException |
string? |
null |
Static bool M(Exception exception): false stops the retries |
RethrowDeclined |
bool |
false |
Rethrow an exception RetryOnException declines unchanged, instead of wrapping it in ResilienceException |
DelayHint |
string? |
null |
Static TimeSpan? M(E error) and/or TimeSpan? M(Exception exception): the next wait, without jitter |
MaxDelayMs |
int |
no cap | Longest wait between attempts, backoff or hint |
[RetryAttempt] on an int? or int parameter of a method under [Retry] passes the retry number instead of the caller's argument: null, 1, 2, … for int?, and 0, 1, 2, … for int.
[Timeout]
[Timeout(Ms = 5_000)]
| Property | Type | Default | Description |
|---|---|---|---|
Ms |
int |
required | Total operation timeout wrapping all retries and backoff |
[RateLimit]
[RateLimit(MaxPerSecond = 100, BurstSize = 10, Scope = RateLimitScope.Shared)]
| Property | Type | Default | Description |
|---|---|---|---|
MaxPerSecond |
int |
required | Token refill rate |
BurstSize |
int |
1 |
Initial and peak token count |
Scope |
RateLimitScope |
Shared |
Shared = singleton per interface; Instance = per proxy |
[CircuitBreaker]
[CircuitBreaker(MaxFailures = 5, ResetMs = 1_000, HalfOpenProbes = 1, Fallback = nameof(FetchFallback))]
| Property | Type | Default | Description |
|---|---|---|---|
MaxFailures |
int |
5 |
Consecutive failures that trip Closed → Open |
ResetMs |
int |
1_000 |
Delay before Open → HalfOpen |
HalfOpenProbes |
int |
1 |
Successes required to close from HalfOpen |
Fallback |
string? |
null |
Fallback method name — called when circuit is Open |
Method-level overrides
Apply attributes directly to methods to override the interface-level config for that method only:
[Retry(MaxAttempts = 3, BackoffMs = 200)]
public interface IExternalService
{
ValueTask<string> FetchAsync(string id, CancellationToken ct); // uses interface-level
[Retry(MaxAttempts = 1)] // POST is not idempotent — one attempt only
ValueTask PostAsync(string data, CancellationToken ct);
}
Method-level attributes shadow interface-level ones entirely for that method — they are not additive.
Failure surface
| Return type | On failure |
|---|---|
ValueTask<T> / Task<T> |
ResilienceException thrown with Policy property |
ValueTask<Result<T>> / Task<Result<T>>, also Result and Result<T, ResilienceError> |
A failure of that type returned — no throw |
ValueTask<Result<T, E>> / Task<Result<T, E>>, other E |
The inner call's own Result returned; see Result Return Types |
try
{
var result = await service.FetchAsync("id", ct);
}
catch (ResilienceException ex) when (ex.Policy == ResiliencePolicy.CircuitBreaker)
{
// circuit was open and no fallback was configured
}
Diagnostics
| ID | Severity | Description |
|---|---|---|
| ZR0001 | Error | Fallback method not found or signature mismatch |
| ZR0002 | Warning | Timeout configured but method has no CancellationToken |
| ZR0003 | Error | Policy cannot build a failure for this Result<T, E> error type |
| ZR0004 | Error | Invalid policy attribute value |
| ZR0006 | Warning | Policy not applied to method |
| ZR0007 | Error | Interface shape not supported by the resilience generator |
| ZR0008 | Error | Invalid ZeroAllocGeneratedAccessibility value |
| ZR0009 | Error | Retry member not found or signature mismatch |
| ZR0010 | Error / Warning | Result-aware retry cannot apply to method |
| ZR0011 | Warning | [RetryAttempt] has no effect without [Retry] |
| ZR0012 | Error | [RetryAttempt] parameter type not supported |
| ZR0013 | Warning | RethrowDeclined has no effect |
Documentation
Full docs live in docs/:
- Getting Started
- Attribute Reference
- Source Generator
- Performance
- Core concepts: Retry · Timeout · Rate Limit · Circuit Breaker · Execution Order
- Guides: Fallback · Method-Level Overrides · Result Return Types · DI Registration · Migrating to 2.0
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- ZeroAlloc.Results (>= 1.2.2)
- ZeroAlloc.StateMachine (>= 1.5.3)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- ZeroAlloc.Results (>= 1.2.2)
- ZeroAlloc.StateMachine (>= 1.5.3)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- ZeroAlloc.Results (>= 1.2.2)
- ZeroAlloc.StateMachine (>= 1.5.3)
NuGet packages (10)
Showing the top 5 NuGet packages that depend on ZeroAlloc.Resilience:
| Package | Downloads |
|---|---|
|
ZeroAlloc.Scheduling
Source-generated zero-allocation background job scheduling for .NET. |
|
|
ZeroAlloc.Outbox
Source-generated transactional outbox for .NET. |
|
|
ZeroAlloc.Rest.Resilience
Wires ZeroAlloc.Rest clients through ZeroAlloc.Resilience proxies. Annotate a [ZeroAllocRestClient] interface with [Retry]/[Timeout]/[CircuitBreaker]; call AddRestResilience to register the resilience-wrapped HTTP client. |
|
|
ZeroAlloc.Scheduling.Resilience
ZeroAlloc.Resilience bridge for ZeroAlloc.Scheduling — wraps IJobTypeExecutor implementations in a Resilience-generated proxy. |
|
|
AI.Sentinel
Security monitoring middleware for IChatClient — prompt injection, hallucination, and operational anomaly detection with an intervention engine. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.3.0 | 1,759 | 9/27/2026 |
| 3.2.0 | 2,106 | 9/26/2026 |
| 3.1.0 | 2,924 | 9/25/2026 |
| 3.0.0 | 55 | 9/25/2026 |
| 2.0.1 | 743 | 9/25/2026 |
| 2.0.0 | 602 | 9/24/2026 |
| 1.3.8 | 65 | 9/24/2026 |
| 1.3.7 | 51 | 9/24/2026 |
| 1.3.6 | 97 | 9/24/2026 |
| 1.3.5 | 54 | 9/24/2026 |
| 1.3.4 | 1,440 | 9/20/2026 |
| 1.3.3 | 217 | 9/20/2026 |
| 1.3.2 | 5,226 | 9/19/2026 |
| 1.3.1 | 3,513 | 8/12/2026 |
| 1.3.0 | 6,148 | 5/13/2026 |
| 1.2.2 | 253 | 5/12/2026 |
| 1.2.1 | 345 | 5/3/2026 |
| 1.2.0 | 175 | 5/1/2026 |
| 1.1.3 | 114 | 5/1/2026 |
| 1.1.2 | 119 | 4/29/2026 |