Kevlar 0.2.1

dotnet add package Kevlar --version 0.2.1
                    
NuGet\Install-Package Kevlar -Version 0.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Kevlar" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Kevlar" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Kevlar" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Kevlar --version 0.2.1
                    
#r "nuget: Kevlar, 0.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Kevlar@0.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Kevlar&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Kevlar&version=0.2.1
                    
Install as a Cake Tool

Kevlar

Fast, allocation-conscious resilience for .NET. Retries, circuit breakers, timeouts, rate limiting, concurrency limiting, hedging and fallbacks — composed through one fluent Shield API.

using Kevlar;

var shield = Shield
    .Timeout(TimeSpan.FromSeconds(30))                    // total budget for the whole operation
    .Retry(3)                                             // exponential backoff + jitter, out of the box
    .CircuitBreaker(5, breakDuration: TimeSpan.FromSeconds(30));

var user = await shield.ExecuteAsync(ct => LoadUserAsync(id, ct), cancellationToken);

Build a shield once, reuse it everywhere. Shields are immutable and thread-safe, and ordinary Task-returning methods flow straight in — no ValueTask wrapping.

And when the scenario stops being simple, the API doesn't change shape — it deepens. Typed handling clauses, options overloads and delegate hooks carry the same fluent chain all the way up:

var monitor = new CircuitBreakerMonitor();

var search = Shield.For<HttpResponseMessage>()
    .When<HttpRequestException>()
    .Or<TimeoutExceededException>()
    .OrResult(r => (int)r.StatusCode is >= 500 or 429)               // results are failures too
    .Fallback((outcome, ct) => cache.GetCachedResultsAsync(ct))      // last resort — sees exactly what failed
    .Retry(o =>
    {
        o.MaxRetries = 4;
        o.Backoff = Backoff.Exponential(TimeSpan.FromMilliseconds(200), maxDelay: TimeSpan.FromSeconds(5));
        o.DelayGenerator = e => e.Outcome.Result?.Headers.RetryAfter?.Delta;   // server knows best…
        o.MaxDelay = TimeSpan.FromSeconds(10);                                 // …within reason
        o.OnRetry = e => logger.LogWarning("search retry {Attempt} in {Delay}", e.Attempt, e.Delay);
    })
    .CircuitBreaker(o =>
    {
        o.FailureRatio = 0.5;                            // open at ≥50% failures…
        o.MinimumThroughput = 20;                        // …across ≥20 calls…
        o.SamplingWindow = TimeSpan.FromSeconds(30);     // …in a rolling 30s window
        o.Monitor = monitor;                             // ops handle: monitor.Isolate() / monitor.Reset()
    })
    .Hedge(maxAttempts: 2, delay: TimeSpan.FromMilliseconds(150))    // race a second attempt on slow p99s
    .Timeout(TimeSpan.FromSeconds(2))                                // per-attempt budget
    .WithName("search");                                             // tags metrics and ToString()

One clause up top decides what "failure" means for every strategy below it — exceptions and result values alike. The result is still just a Shield<HttpResponseMessage>: immutable, reusable, and it prints its own pipeline when you log it.

Why Kevlar?

  • Intuitive first. Shield.When<TimeoutException>().Retry(3) reads like what it does. No context pooling ceremony, no predicate-builder classes, no options objects for the simple cases — and full options objects when you want them.
  • Fast. Outcomes flow between pipeline layers as structs instead of thrown exceptions; contexts are pooled internally; state-passing overloads eliminate closures; ValueTask end to end.
  • Production defaults. Shield.Retry(3) gives you exponential backoff with jitter capped at 30s — the thing you'd have configured anyway.
  • Hard to hold wrong. Impossible chain orders throw at build time with the fix in the message, and the Kevlar.Analyzers package flags delegates that ignore their CancellationToken at compile time.
  • Observable out of the box. shield.ToString() prints the whole pipeline; every shield publishes metrics through a built-in Meter — no telemetry package, no setup.
  • Composable. Shields merge with Wrap and Compose, chain fluently, and stateful strategies (breakers, limiters) intentionally share their state wherever the same shield instance is reused.
  • Broad reach. netstandard2.0 (covers .NET Framework 4.6.2+) and net10.0 targets.

Packages

Package Purpose
Kevlar The core: all strategies
Kevlar.Extensions.DependencyInjection Named shields, config-bound shields + IKevlarRegistry for Microsoft DI
Kevlar.Extensions.Http HttpClientFactory integration, transient-fault handling, Retry-After support
Kevlar.Analyzers Roslyn analyzers that catch resilience mistakes at compile time

The five-minute tour

Handling clauses

Tell reactive strategies (retry, circuit breaker, hedging, fallback) what counts as a failure — When starts a clause, Or extends it:

// Exceptions
var shield = Shield
    .When<HttpRequestException>()
    .Or<TimeoutExceededException>()
    .OrWhen(ex => ex is IOException { Message: var m } && m.Contains("pipe"))
    .Retry(5);

// Results too — lift into a typed shield with For<T>
var http = Shield.For<HttpResponseMessage>()
    .When<HttpRequestException>()
    .OrResult(r => (int)r.StatusCode >= 500)
    .Retry(3);

// The most common result check has a shorthand:
Shield.For<User?>().WhenDefault().Retry(2);   // retry null results

With no handling clause, the default is: any exception except OperationCanceledException. A clause applies to the strategy it creates and to every reactive strategy chained after it — through For<T>(), Wrap and Compose too — until you write a new clause.

Retry

Shield.Retry(3);                                          // exponential + jitter (250ms base, 30s cap)
Shield.Retry(3, Backoff.Constant(TimeSpan.FromSeconds(1)));
Shield.Retry(3, Backoff.Linear(TimeSpan.FromMilliseconds(500)));
Shield.RetryForever(Backoff.Exponential(TimeSpan.FromSeconds(1), maxDelay: TimeSpan.FromMinutes(1)));

Shield.Retry(o =>
{
    o.MaxRetries = 5;
    o.Backoff = Backoff.Custom(attempt => TimeSpan.FromMilliseconds(100 * attempt));
    o.MaxDelay = TimeSpan.FromSeconds(10);   // absolute cap — even over DelayGenerator output
    o.OnRetry = e => logger.LogWarning(e.Exception, "Retry {Attempt} after {Delay}", e.Attempt, e.Delay);
    o.DelayGenerator = e => /* return a TimeSpan to override the computed delay, or null */ null;
});

On a typed Shield<T>, retry events are typed too: e.Outcome is an Outcome<T> — no boxed object results, no casting.

Circuit breaker

// Simple: open after N consecutive failures
Shield.CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));

// Sampling: open when ≥50% of calls fail within a rolling window
var monitor = new CircuitBreakerMonitor();
Shield.CircuitBreaker(o =>
{
    o.FailureRatio = 0.5;
    o.MinimumThroughput = 20;
    o.SamplingWindow = TimeSpan.FromSeconds(30);
    o.BreakDuration = TimeSpan.FromSeconds(15);
    o.Monitor = monitor;                                  // observe + manual control
    o.OnStateChanged = c => logger.LogWarning("Circuit {From} -> {To}", c.From, c.To);
});

monitor.State;      // Closed / Open / HalfOpen / Isolated
monitor.Isolate();  // force open (maintenance switch)
monitor.Reset();    // close and clear metrics

Open circuits reject with CircuitOpenException (carrying RetryAfter). After the break duration, one probe execution decides whether to close or re-open.

Timeout

Shield.Timeout(TimeSpan.FromSeconds(10));

Cooperative: the delegate receives a cancellation token that fires on timeout — always use the token you're handed (the Kevlar.Analyzers package warns when you don't). Exceeding the budget surfaces TimeoutExceededException, which retry clauses can handle.

Rate limit & concurrency limit

Shield.RateLimit(100, perWindow: TimeSpan.FromSeconds(1));   // token bucket, burst = 100
Shield.RateLimit(o => { o.Permits = 100; o.Window = TimeSpan.FromSeconds(1); o.QueueLimit = 20; });

Shield.ConcurrencyLimit(maxConcurrency: 10, maxQueue: 20);   // the classic bulkhead pattern

Rejections throw RateLimitExceededException (with a RetryAfter estimate) and ConcurrencyLimitExceededException. With QueueLimit > 0, rate-limited executions wait for their reserved permit instead of failing.

Hedging

// Fire a second attempt if the first hasn't answered within 100ms; fastest wins, losers are cancelled.
Shield.Hedge(maxAttempts: 2, delay: TimeSpan.FromMilliseconds(100));

Delay = TimeSpan.Zero races all attempts at once; Timeout.InfiniteTimeSpan hedges only on failure. A handled failure always launches the next attempt immediately. Your delegate must be safe to invoke concurrently.

Fallback

var shield = Shield.For<Config>()
    .When<HttpRequestException>()
    .Fallback(Config.Default);

// Or compute it, with access to the typed failure:
.Fallback((outcome, ct) =>
{
    logger.LogError(outcome.Exception, "Using cached config");
    return new ValueTask<Config>(cache.Get());
});

// Void executions have their own fallback on the plain Shield:
Shield.When<MessagingException>()
    .Fallback((exception, ct) => deadLetter.PublishAsync(exception, ct));

Fallback belongs before the strategies it recovers from (first = outermost). Chain it after a retry with the same clause and Kevlar throws at build time — that order silently disables the retry, so it refuses to build it.

Composition

The first strategy in a chain is the outermost — the same rule as ASP.NET middleware:

Shield
    .Timeout(TimeSpan.FromSeconds(30))   // 1. total budget around everything below
    .Retry(3)                            // 2. retries happen inside that budget
    .CircuitBreaker(5, TimeSpan.FromSeconds(30))
    .Timeout(TimeSpan.FromSeconds(5));   // 4. each individual attempt gets 5s

Merge independently defined shields:

var breaker  = Shield.CircuitBreaker(5, TimeSpan.FromSeconds(30));   // built once — holds the circuit state
var reads    = Shield.Retry(3).Wrap(breaker);
var writes   = Shield.Timeout(TimeSpan.FromSeconds(5)).Wrap(breaker);
// reads and writes share ONE circuit: failures through either trip both.

var combined = Shield.Compose(timeoutShield, retryShield, breakerShield);  // first = outermost

That's the state-sharing rule in one line: strategy state lives with the shield instance that created it. Reuse the instance to share a circuit or a rate limiter; build a new one for fresh state.

Every shield describes itself — log it at startup:

logger.LogInformation("using {Shield}", shield);
// github: Timeout(30s) → Retry(3, exponential 250ms ×2 +jitter ≤30s) → CircuitBreaker(5 consecutive, break 30s)

Executing

await shield.ExecuteAsync(ct => FetchAsync(ct), cancellationToken);   // Task or ValueTask — both just work
await shield.ExecuteAsync(ct => SaveAsync(ct), cancellationToken);    // async void
shield.Execute(ct => ComputeSync(ct));                                // sync (same shield!)

// Zero-closure hot path: thread your state instead of capturing it
await shield.ExecuteAsync((client, id), static (s, ct) => s.client.GetUserAsync(s.id, ct), ct);

// No-throw execution: inspect the outcome instead
Outcome<User> outcome = await shield.ExecuteOutcomeAsync(ct => LoadAsync(ct));
if (!outcome.IsSuccess) logger.LogError(outcome.Exception, "gave up");

The same shield serves any result type, sync or async. (One exception: hedging is inherently concurrent and requires async execution.)

Dependency injection

services.AddShield("github", Shield.Timeout(TimeSpan.FromSeconds(10)).Retry(3));
services.AddShield<HttpResponseMessage>("downstream",
    sp => HttpShield.WhenTransient().Retry(3).WithName("downstream"));

// Or bind the whole shield from configuration — tunable without a redeploy:
services.AddShield("github", builder.Configuration.GetSection("Resilience:GitHub"));

// Consume via the registry…
var shield = registry.GetShield("github");                       // IKevlarRegistry
// …or as a keyed service
public sealed class GitHubClient([FromKeyedServices("github")] Shield shield) { }

HTTP

services.AddHttpClient("api")
    .AddStandardShield();     // 30s total timeout → 3 jittered retries (honouring Retry-After,
                              // disposing retried responses) → circuit breaker → 10s attempt timeout

// Or bring your own:
services.AddHttpClient("api")
    .AddShield(HttpShield.WhenTransient()        // HttpRequestException, attempt timeouts, 5xx, 408, 429
        .Retry(o => { o.MaxRetries = 4; o.DelayGenerator = HttpShield.RetryAfter; })
        .CircuitBreaker(o => o.FailureRatio = 0.5));

Observability

On .NET 8+ every shield publishes metrics through a Meter named "Kevlar" with zero configuration — executions, retries, timeouts, hedges, fallbacks, rejections and circuit transitions, tagged with the shield's WithName name. Subscribe with AddMeter(KevlarDiagnostics.MeterName).

And dotnet add package Kevlar.Analyzers adds compile-time checks — starting with KEV001: an execution delegate that ignores the CancellationToken it is handed (the most common way to defeat a timeout).

Custom strategies

Everything in Kevlar is a Strategy — middleware over an Outcome<T> pipeline. Write your own:

public sealed class LoggingStrategy(ILogger logger) : Strategy
{
    public override async ValueTask<Outcome<T>> ExecuteAsync<T, TState>(
        Continuation<T, TState> next, KevlarContext context)
    {
        var start = context.TimeProvider.GetTimestamp();
        var outcome = await next.InvokeAsync(context);
        logger.LogInformation("{Shield} took {Elapsed}", context.ShieldName,
            context.TimeProvider.GetElapsedTime(start));
        return outcome;
    }

    public override string Describe() => "Logging";
}

var shield = Shield.Use(new LoggingStrategy(logger)).Retry(3);

Strategies return failures as outcomes rather than throwing, so outer strategies can react to them. Invoke next zero times (short-circuit), once (decorate), or many times (retry/hedge).

Testing your shields

Every delay, timeout and time window runs on a TimeProvider:

var time = new FakeTimeProvider();   // Microsoft.Extensions.TimeProvider.Testing
var shield = Shield.Retry(3, Backoff.Constant(TimeSpan.FromSeconds(10))).WithTimeProvider(time);

var pending = shield.ExecuteAsync(ct => FlakyAsync(ct)).AsTask();
time.Advance(TimeSpan.FromSeconds(10));                  // no real waiting in tests

Coming from Polly?

Polly v8 Kevlar
new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions { … }).Build() Shield.Retry(3)
ShouldHandle = new PredicateBuilder().Handle<T>() Shield.When<T>().… (ambient for the whole chain)
ResiliencePipeline / ResiliencePipeline<T> Shield / Shield<T>
ResilienceContextPool.Shared.Get(...) + Return automatic — contexts are pooled internally
BrokenCircuitException CircuitOpenException (with RetryAfter)
TimeoutRejectedException TimeoutExceededException
CircuitBreakerManualControl + StateProvider one CircuitBreakerMonitor
AddConcurrencyLimiter(10, 20) Shield.ConcurrencyLimit(10, maxQueue: 20)
Delegates must return ValueTask Task-returning methods flow straight in
Retry default: constant 2s, no jitter exponential + jitter, 30s cap
First strategy added is outermost same rule — pipelines translate 1:1

Performance

Kevlar is benchmarked against Polly v8 across every strategy on every merge to main — happy paths, failure paths, and composed pipelines. The results are published automatically to the Benchmarks page in the docs.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Kevlar:

Package Downloads
Kevlar.Extensions.Http

HttpClientFactory integration for Kevlar: resilient HTTP handlers with transient-fault handling, Retry-After aware retries and a production-ready standard pipeline.

Kevlar.Extensions.DependencyInjection

Microsoft.Extensions.DependencyInjection integration for Kevlar: named shield registration and the IKevlarRegistry.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 42 8/20/2026
0.2.0 46 8/20/2026
0.1.0 34 8/20/2026