Fast, allocation-conscious resilience for .NET. Kevlar brings retries, circuit breakers, timeouts, rate limiting, concurrency limiting, hedging and fallbacks together in a fluent API.
Resilience code should explain how a call is protected, not make you decode a framework. With
Kevlar, you build an immutable Shield, reuse it, and use it with ordinary delegates.
Documentation · Strategies · API Reference · Benchmarks
dotnet add package KevlarInstall all coupled Kevlar.* packages at the same version. NuGet reports partial upgrades with
NU1605 or NU1608; see the package lockstep policy.
using Kevlar;
var shield = Shield.Retry(3);
using var client = new HttpClient();
using var response = await shield.ExecuteAsync(
ct => client.GetAsync("https://example.com", ct));Retry(3) means three retries after the initial call: up to 4 total attempts. Its default backoff
is exponential from 250 ms with factor 2, equal jitter, and a 30-second cap. It retries ordinary
exceptions such as HttpRequestException, but treats the TaskCanceledException from
HttpClient.Timeout as cancellation. To retry that timeout and HTTP 5xx/429 responses, use
HttpShield.WhenTransient() from Kevlar.Extensions.Http. The cancellation token passed to your
delegate is important—it is how timeouts and abandoned attempts stop the underlying work.
When you combine strategies, the first strategy is the outermost, just like ASP.NET middleware:
var productionShield = Shield
.Timeout(TimeSpan.FromSeconds(30))
.Retry(3)
.CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));That reads in execution order: the 30-second timeout wraps the retries, which wrap the circuit breaker.
Build shields once and reuse them. They are immutable and thread-safe. Reuse also matters for stateful strategies: calls made through the same shield share its circuit breaker and limiter state.
- The common case stays small. Start with
Shield.Retry(3); use options and callbacks when the situation genuinely needs them. - Failures can be exceptions or results. Retry an
HttpRequestException, an HTTP 500 response, or both, without changing the shape of the pipeline. - Composition is explicit. Chain strategies, or combine existing shields with
WrapandCompose. The first strategy is always the outermost. - State can be isolated by key. Partitioned shields retain independent breaker, limiter, and queue state per tenant, endpoint, or other bounded key.
- It is designed for hot paths. Struct outcomes, pooled contexts, state-passing overloads and
ValueTaskkeep overhead and allocations low. Browse the benchmark suite or the published comparative BenchmarkDotNet results. - Production concerns are built in. Shields support
TimeProvider, describe their own pipeline, publish metrics through theKevlarmeter, and can emit structuredILoggerevents throughKevlar.Extensions.Logging. Hook exceptions never replace the protected outcome; observe them throughKevlarDiagnostics.OnCallbackError, logging, orKevlar.Testing.TelemetryRecorder. Built-in analyzers catch cancellation and pipeline mistakes at compile time.
Every sample is a small net8.0;net10.0 application with a --smoke mode used by CI:
samples/ConsoleRetry— minimal retry and recovery.samples/WebApi— ASP.NET Core, named DI shields,AddStandardShield, and metrics.samples/GrpcClient— transient gRPC unary retries.samples/PartitionedWorker— bounded per-tenant shield state.samples/ChaosTesting— deterministic, operation-scoped fault injection.
Build all samples with dotnet build samples/Samples.slnx -c Release, or follow the command in a
sample's README to run one directly.
Reactive strategies handle ordinary exceptions by default, excluding cancellation, Kevlar's fail-fast rejections, and fatal runtime failures. A handling clause lets you be more precise:
var search = Shield.For<HttpResponseMessage>()
.When<HttpRequestException>()
.Or<TimeoutExceededException>()
.OrResult(response => (int)response.StatusCode is 429 or >= 500)
.Fallback((outcome, ct) => cache.GetCachedResultsAsync(ct))
.Retry(3)
.CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));A clause is ambient. It applies to the strategy it is attached to and to every reactive
strategy chained after it, until a new clause replaces it, WithDefaultHandling() resets it, or
Wrap/Compose seals it. Above, the fallback, the retry and the circuit breaker all react to
the same three conditions. Nothing repeats the predicate per strategy:
var api = Shield
.When<HttpRequestException>()
.Retry(3) // retries HttpRequestException
.CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));
// the breaker inherits the clause above: only HttpRequestException counts toward tripping itTyped shields keep result handling strongly typed, including callback events and Outcome<T>
values.
The first strategy is the outermost, just like ASP.NET middleware:
var shield = Shield
.Timeout(TimeSpan.FromSeconds(30)) // total budget
.Retry(3) // retry within that budget
.CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30))
.Timeout(TimeSpan.FromSeconds(5)); // budget for each attemptThis rule makes the important questions visible: is a timeout per attempt or for the whole call?
Does fallback wrap retry? Are two clients meant to share one circuit? See
composition for Wrap, Compose and the
state-sharing rules.
Kevlar.Extensions.Http provides a ready-to-use HttpClientFactory pipeline. In an ASP.NET Core
app using Microsoft.NET.Sdk.Web:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder();
builder.Services.AddHttpClient("api")
.AddStandardShield();The standard shield has a 30-second total timeout, three jittered retries that honour
Retry-After, a circuit breaker, and a 10-second timeout per attempt. You can configure every
part or supply your own shield. POST, PATCH, and custom methods remain single-attempt unless you
explicitly enable replay for operations that are safe to repeat.
Kevlar.Extensions.DependencyInjection adds named, configuration-bound shields and
IKevlarRegistry. In a standalone console project, install the concrete
Microsoft.Extensions.DependencyInjection package as well; it provides BuildServiceProvider():
using System;
using Kevlar;
using Kevlar.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddShield("database", Shield
.Timeout(TimeSpan.FromSeconds(10))
.Retry(3));
using var serviceProvider = services.BuildServiceProvider();
var registry = serviceProvider.GetRequiredService<IKevlarRegistry>();
var databaseShield = registry.GetShield("database");Kevlar is a focused Polly alternative for teams that prefer immutable pipelines, explicit reading-order composition, and allocation-conscious hot paths. It is not API-compatible with Polly; the migration guide maps the concepts side by side.
| Package | What it adds |
|---|---|
Kevlar |
Core strategies, Shield API, and diagnostics-only analyzers |
Kevlar.Chaos |
Controlled latency, faults, outcomes and custom behaviour |
Kevlar.Extensions.DependencyInjection |
Named and configuration-bound shields for Microsoft DI |
Kevlar.Extensions.Http |
HttpClientFactory integration, request replay and transient-fault handling |
Kevlar.Extensions.Logging |
Structured ILogger events for every built-in strategy |
Kevlar.Extensions.Grpc |
gRPC client resilience for unary and streaming calls |
Kevlar.Extensions.RateLimiting |
Adapters for System.Threading.RateLimiting and custom leases |
Kevlar.Testing |
Pipeline assertions, state snapshots and deterministic time helpers |
| Package | Target frameworks |
|---|---|
Kevlar |
netstandard2.0; net8.0; net10.0 |
Kevlar.Chaos |
netstandard2.0; net8.0; net10.0 |
Kevlar.Extensions.DependencyInjection |
netstandard2.0; net8.0; net10.0 |
Kevlar.Extensions.Grpc |
netstandard2.0; netstandard2.1; net8.0; net10.0 |
Kevlar.Extensions.Http |
netstandard2.0; net8.0; net10.0 |
Kevlar.Extensions.Logging |
netstandard2.0; net8.0; net10.0 |
Kevlar.Extensions.RateLimiting |
netstandard2.0; net8.0; net10.0 |
Kevlar.Testing |
netstandard2.0; net8.0; net10.0 |
The analyzers bundled with Kevlar run in Visual Studio 2022 17.8 or later and the .NET 8.0.100
SDK or later. Older compiler hosts skip the analyzer assets; the runtime library remains
available. Kevlar.Testing supports callback recording on netstandard2.0, but deterministic time
and metric capture require .NET 8 or later.
The core package depends on Reservoir [1.4.0, 2.0.0) on every target. Its netstandard2.0
asset also uses Microsoft.Bcl.AsyncInterfaces, Microsoft.Bcl.TimeProvider, and
System.Threading.Tasks.Extensions. See the support policy
for integration-package dependency floors, package lockstep, and the release support window.
Kevlar is licensed under the MIT License.
- Follow the getting-started guide.
- Browse the complete API reference.
- Browse the strategy reference.
- Add HTTP resilience, dependency injection, or observability.
- Moving from Polly? The migration guide maps the concepts side by side.
Read the contribution guide, code of conduct, and release notes. Use the guided issue forms for bugs, feature requests, and questions; report vulnerabilities through GitHub's private security advisory form.