Optimize Polly resilience pipelines for maximum performance and minimal allocations
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:
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);
Using static lambdas eliminates closure allocations and can significantly improve performance in hot paths.
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();
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 poolResilienceContext 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 poolResilienceContextPool.Shared.Return(context);
Using ExecuteOutcomeAsync avoids the overhead of exception throwing and catching, which can be significant in high-throughput scenarios.
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); }}
You can also define your pipeline on startup using dependency injection and then use ResiliencePipelineProvider<T> to retrieve the instance.