Skip to main content

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).

note

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.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Empty pipeline — async11.6 ns53.1 ns0 B0 B4.2× faster
Empty pipeline — reference-state baseline11.3 ns0 B
Empty pipeline — caller-seeded context83.1 ns0 B
EmptyOutcomeState4.7 ns0 B
EmptyTaskOutcomeState8.0 ns0 B
Empty pipeline — zero-closure state overload11.4 ns55.3 ns0 B0 B4.5× faster
Empty pipeline — sync6.1 ns32.7 ns0 B0 B3.2× faster
NestedEmptyAsync211 ns0 B
NestedEmptySync160 ns0 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.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Retry(3) — success on first attempt100 ns198 ns0 B24 B1.9× faster
Retry(3) — two failures then success3.00 μs3.46 μs192 B328 Bon par

Timeout

The timeout never fires; this is the cost of arming and disarming the cancellation plumbing on every call.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Timeout(10 s) — completes instantly175 ns173 ns0 B0 Bon par
SynchronousGenerator_HappyPath177 ns0 B
AsynchronousGenerator_HappyPath2.05 μs554 B
AsyncHookConfigured_HappyPath183 ns0 B

Circuit breaker

Ratio/sampling bookkeeping while closed, and the fast-fail rejection cost while manually isolated (thrown exception included).

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Isolated circuit — fast-fail rejection4.48 μs4.49 μs1.3 KB1.3 KBon par
Ratio breaker, closed — success135 ns219 ns0 B24 B1.4× faster
DynamicDurationConfigured159 ns0 B
AsyncCallbackConfigured156 ns0 B

Fallback

Pass-through when the execution succeeds, and substitution when it throws.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
NoNotification1.89 μs200 B
CompletedAsyncNotification1.88 μs200 B
YieldingAsyncNotification5.42 μs784 B
Fallback — not triggered104 ns103 ns0 B0 Bon par
SynchronousDelegate_Triggered1.82 μs200 B
Fallback — triggered by exception1.89 μs1.87 μs200 B256 Bon par
EmptyVoid26.2 ns0 B
VoidPassThrough111 ns0 B

Rate limit

Uncontended token-bucket permit acquisition — every call is admitted.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Token bucket — uncontended acquire122 ns129 ns0 B0 Bon par
WithHooks_Uncontended124 ns0 B
FrameworkAdapter_Uncontended133 ns0 B
PartitionedFrameworkAdapter_Uncontended153 ns32 B

Concurrency limit

A single caller against a large permit count; acquire/release cost with no queueing.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Concurrency limit — uncontended126 ns184 ns0 B40 B1.3× faster
WithHooks_Uncontended126 ns0 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.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Typed retry — result judged, no retry104 ns151 ns0 B0 B1.4× faster

Composed pipelines

How per-call overhead scales with pipeline depth when nothing goes wrong.

ScenarioKevlarPollyKevlar allocatedPolly allocatedKevlar vs Polly
Timeout → Retry → ratio breaker283 ns537 ns0 B48 B1.9× faster
Token bucket → Timeout → Retry → ratio breaker → Concurrency limit375 ns750 ns0 B88 B2.0× faster
TokenBucketRatioFiveStrategyChainSync334 ns743 ns0 B88 B2.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.