Benchmarks
Kevlar vs Polly v8 across every strategy, measured with BenchmarkDotNet on GitHub Actions and republished automatically by the benchmarks workflow.
Last updated 2026-09-10 13:28 UTC (commit dda402f).
Microbenchmarks on shared CI runners are noisy. The vs Polly column is the median ratio over the most recent runs (up to 10); anything within ±20% is reported as on par. Absolute times move with runner hardware — the ratios are the signal.
Pipeline overhead floor
What an empty pipeline costs per execution — the fixed tax every strategy builds on.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Empty pipeline — async | 11.6 ns | 53.1 ns | 0 B | 0 B | 4.2× faster |
| Empty pipeline — reference-state baseline | 11.3 ns | — | 0 B | — | — |
| Empty pipeline — caller-seeded context | 83.1 ns | — | 0 B | — | — |
| EmptyOutcomeState | 4.7 ns | — | 0 B | — | — |
| EmptyTaskOutcomeState | 8.0 ns | — | 0 B | — | — |
| Empty pipeline — zero-closure state overload | 11.4 ns | 55.3 ns | 0 B | 0 B | 4.5× faster |
| Empty pipeline — sync | 6.1 ns | 32.7 ns | 0 B | 0 B | 3.2× faster |
| NestedEmptyAsync | 211 ns | — | 0 B | — | — |
| NestedEmptySync | 160 ns | — | 0 B | — | — |
Retry
Happy path (judge overhead only) and a recovery path where every call fails twice before succeeding, with backoff disabled so only strategy machinery is measured.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Retry(3) — success on first attempt | 100 ns | 198 ns | 0 B | 24 B | 1.9× faster |
| Retry(3) — two failures then success | 3.00 μs | 3.46 μs | 192 B | 328 B | on par |
Timeout
The timeout never fires; this is the cost of arming and disarming the cancellation plumbing on every call.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Timeout(10 s) — completes instantly | 175 ns | 173 ns | 0 B | 0 B | on par |
| SynchronousGenerator_HappyPath | 177 ns | — | 0 B | — | — |
| AsynchronousGenerator_HappyPath | 2.05 μs | — | 554 B | — | — |
| AsyncHookConfigured_HappyPath | 183 ns | — | 0 B | — | — |
Circuit breaker
Ratio/sampling bookkeeping while closed, and the fast-fail rejection cost while manually isolated (thrown exception included).
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Isolated circuit — fast-fail rejection | 4.48 μs | 4.49 μs | 1.3 KB | 1.3 KB | on par |
| Ratio breaker, closed — success | 135 ns | 219 ns | 0 B | 24 B | 1.4× faster |
| DynamicDurationConfigured | 159 ns | — | 0 B | — | — |
| AsyncCallbackConfigured | 156 ns | — | 0 B | — | — |
Fallback
Pass-through when the execution succeeds, and substitution when it throws.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| NoNotification | 1.89 μs | — | 200 B | — | — |
| CompletedAsyncNotification | 1.88 μs | — | 200 B | — | — |
| YieldingAsyncNotification | 5.42 μs | — | 784 B | — | — |
| Fallback — not triggered | 104 ns | 103 ns | 0 B | 0 B | on par |
| SynchronousDelegate_Triggered | 1.82 μs | — | 200 B | — | — |
| Fallback — triggered by exception | 1.89 μs | 1.87 μs | 200 B | 256 B | on par |
| EmptyVoid | 26.2 ns | — | 0 B | — | — |
| VoidPassThrough | 111 ns | — | 0 B | — | — |
Rate limit
Uncontended token-bucket permit acquisition — every call is admitted.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Token bucket — uncontended acquire | 122 ns | 129 ns | 0 B | 0 B | on par |
| WithHooks_Uncontended | 124 ns | — | 0 B | — | — |
| FrameworkAdapter_Uncontended | 133 ns | — | 0 B | — | — |
| PartitionedFrameworkAdapter_Uncontended | 153 ns | — | 32 B | — | — |
Concurrency limit
A single caller against a large permit count; acquire/release cost with no queueing.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Concurrency limit — uncontended | 126 ns | 184 ns | 0 B | 40 B | 1.3× faster |
| WithHooks_Uncontended | 126 ns | — | 0 B | — | — |
Typed result handling
Retry configured to treat a sentinel result as a failure; the returned value never matches, so this is the per-call cost of judging results.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Typed retry — result judged, no retry | 104 ns | 151 ns | 0 B | 0 B | 1.4× faster |
Composed pipelines
How per-call overhead scales with pipeline depth when nothing goes wrong.
| Scenario | Kevlar | Polly | Kevlar allocated | Polly allocated | Kevlar vs Polly |
|---|---|---|---|---|---|
| Timeout → Retry → ratio breaker | 283 ns | 537 ns | 0 B | 48 B | 1.9× faster |
| Token bucket → Timeout → Retry → ratio breaker → Concurrency limit | 375 ns | 750 ns | 0 B | 88 B | 2.0× faster |
| TokenBucketRatioFiveStrategyChainSync | 334 ns | 743 ns | 0 B | 88 B | 2.1× faster |
Environment
- INTEL XEON PLATINUM 8573C
- .NET 10.0.12 (10.0.12, 10.0.1226.42308), BenchmarkDotNet 0.15.8
- Times are medians; allocations are per operation.
Reproduce
dotnet run -c Release --project benchmarks/Kevlar.Benchmarks -- --filter '*'
As always with microbenchmarks: measure your own workload before optimizing around these numbers. Nanosecond differences matter in tight loops and high-throughput services; they don't matter around a 50 ms network call.