Performance
Polly is fast and avoids allocations wherever possible. We use a comprehensive set of performance benchmarks to monitor Polly's performance.
Here's an example of results from an advanced pipeline composed of the following strategies:
- Timeout (outer)
- Rate limiter
- Retry
- Circuit breaker
- Timeout (inner)
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|
Execute policy v7 | 2.277 μs | 0.0133 μs | 0.0191 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 |
Execute pipeline v8 | 2.089 μs | 0.0105 μs | 0.0157 μs | 0.92 | 0.01 | - | 40 B | 0.01 |
Compared to older versions, Polly v8 is both faster and more memory efficient.
Performance tips
If you're aiming for the best performance with Polly, consider these tips:
Use static lambdas
Lambdas capturing variables from their outer scope will allocate on every execution. Polly provides tools to avoid this overhead, as shown in the example below:
// This call allocates for each invocation since the "userId" variable is captured from the outer scope.
await resiliencePipeline.ExecuteAsync(
cancellationToken => GetMemberAsync(userId, cancellationToken),
cancellationToken);
// This approach uses a static lambda, avoiding allocations.
// The "userId" is passed to the execution via the state argument, and the lambda consumes it as the first
// parameter passed to the GetMemberAsync() method. In this case, userIdAsState and userId are the same value.
await resiliencePipeline.ExecuteAsync(
static (userIdAsState, cancellationToken) => GetMemberAsync(userIdAsState, cancellationToken),
userId,
cancellationToken);
Use switch expressions for predicates
The PredicateBuilder
maintains a list of all registered predicates. To determine whether the results should be processed, it iterates through this list. Using switch expressions can help you bypass this overhead.
// Here, PredicateBuilder is used to configure which exceptions the retry strategy should handle.
new ResiliencePipelineBuilder()
.AddRetry(new()
{
ShouldHandle = new PredicateBuilder()
.Handle<SomeExceptionType>()
.Handle<InvalidOperationException>()
.Handle<HttpRequestException>()
})
.Build();
// For optimal performance, it's recommended to use switch expressions instead of PredicateBuilder.
new ResiliencePipelineBuilder()
.AddRetry(new()
{
ShouldHandle = args => args.Outcome.Exception switch
{
SomeExceptionType => PredicateResult.True(),
InvalidOperationException => PredicateResult.True(),
HttpRequestException => PredicateResult.True(),
_ => PredicateResult.False()
}
})
.Build();
Execute callbacks without throwing exceptions
Polly provides the ExecuteOutcomeAsync
API, returning results as Outcome<T>
. The Outcome<T>
might contain an exception instance, which you can check without it being thrown. This is beneficial when employing exception-heavy resilience strategies, like circuit breakers.
// Execute GetMemberAsync and handle exceptions externally.
try
{
await pipeline.ExecuteAsync(cancellationToken => GetMemberAsync(id, cancellationToken), cancellationToken);
}
catch (Exception e)
{
// Log the exception here.
logger.LogWarning(e, "Failed to get member with id '{id}'.", id);
}
// The example above can be restructured as:
// Acquire a context from the pool
ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken);
// Instead of wrapping pipeline execution with try-catch, use ExecuteOutcomeAsync(...).
// Certain strategies are optimized for this method, returning an exception instance without actually throwing it.
Outcome<Member> outcome = await pipeline.ExecuteOutcomeAsync(
static async (context, state) =>
{
// The callback for ExecuteOutcomeAsync must return an Outcome<T> instance. Hence, some wrapping is needed.
try
{
return Outcome.FromResult(await GetMemberAsync(state, context.CancellationToken));
}
catch (Exception e)
{
return Outcome.FromException<Member>(e);
}
},
context,
id);
// Handle exceptions using the Outcome<T> instance instead of try-catch.
if (outcome.Exception is not null)
{
logger.LogWarning(outcome.Exception, "Failed to get member with id '{id}'.", id);
}
// Release the context back to the pool
ResilienceContextPool.Shared.Return(context);
Reuse resilience pipeline instances
Creating a resilience pipeline can be resource-intensive, so it's advisable not to discard the instance after each use. Instead, you can either cache the resilience pipeline or use the GetOrAddPipeline(...)
method from ResiliencePipelineRegistry<T>
to cache the pipeline dynamically:
public sealed class MyApi
{
private readonly ResiliencePipelineRegistry<string> _registry;
// Share a single instance of the registry throughout your application.
public MyApi(ResiliencePipelineRegistry<string> registry)
{
_registry = registry;
}
public async Task UpdateData(CancellationToken cancellationToken)
{
// Get or create the pipeline, and then cache it for subsequent use.
// Choose a sufficiently unique key to prevent collisions.
var pipeline = _registry.GetOrAddPipeline("my-app.my-api", builder =>
{
builder.AddRetry(new()
{
ShouldHandle = new PredicateBuilder()
.Handle<InvalidOperationException>()
.Handle<HttpRequestException>()
});
});
await pipeline.ExecuteAsync(async token =>
{
// Place your logic here
},
cancellationToken);
}
}
Note
You can also define your pipeline on startup using dependency injection and then use ResiliencePipelineProvider<T>
to retrieve the instance.