Polly v8 resilience for gRPC .NET — retry, timeout, and circuit-breaker for any gRPC unary call, plus a built-in GrpcTransientErrors predicate covering the most common transient status codes. Works with any generated gRPC client, GrpcChannel, or CallInvoker.
// Before
var reply = await client.SayHelloAsync(new HelloRequest { Name = "world" });
// After — automatic retry + timeout on every call
var resilient = channel.WithPolly(pipeline =>
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
ShouldHandle = GrpcTransientErrors.IsTransient, // built-in ✔
})
.AddTimeout(TimeSpan.FromSeconds(10)));
var reply = await resilient.ExecuteAsync(ct =>
client.SayHelloAsync(new HelloRequest { Name = "world" }, cancellationToken: ct));dotnet add package PollyGrpcTargets net6.0, net8.0, and net9.0.
Dependencies: Polly.Core 8.*, Grpc.Net.Client 2.*, Microsoft.Extensions.DependencyInjection.Abstractions 8.*
Knowing which gRPC status codes are safe to retry is the hard part. PollyGrpc ships GrpcTransientErrors.IsTransient so you never have to look them up.
new RetryStrategyOptions
{
MaxRetryAttempts = 3,
ShouldHandle = GrpcTransientErrors.IsTransient,
}| Code | Name | Description |
|---|---|---|
4 |
DeadlineExceeded | Request timed out before server could respond |
8 |
ResourceExhausted | Quota or rate limit exceeded (like HTTP 429) |
10 |
Aborted | Operation aborted — transaction conflict; safe to retry |
14 |
Unavailable | Server temporarily unavailable — most common transient gRPC error |
Tip:
StatusCode.Internal (13)can also be transient (connection reset). If you see it in logs, extend the predicate:
var myErrors = GrpcTransientErrors.StatusCodes.ToHashSet();
myErrors.Add(StatusCode.Internal);
new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<RpcException>(ex => myErrors.Contains(ex.StatusCode))
}Wrap your existing GrpcChannel and pass any lambda that makes a gRPC call:
using PollyGrpc;
var channel = GrpcChannel.ForAddress("https://my-service:5001");
var resilient = channel.WithPolly(pipeline =>
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = GrpcTransientErrors.IsTransient,
})
.AddTimeout(TimeSpan.FromSeconds(10)));
var client = new Greeter.GreeterClient(channel);
// Unary call — pass AsyncUnaryCall directly
var reply = await resilient.ExecuteAsync(ct =>
client.SayHelloAsync(new HelloRequest { Name = "world" }, cancellationToken: ct));
// Or pass .ResponseAsync explicitly
var reply2 = await resilient.ExecuteAsync(ct =>
client.SayHelloAsync(new HelloRequest { Name = "world" }, cancellationToken: ct).ResponseAsync);The interceptor approach integrates transparently at the gRPC channel level — no changes to call sites needed:
var options = new PollyGrpcOptions
{
MaxRetries = 3,
BaseDelay = TimeSpan.FromMilliseconds(200),
CallTimeout = TimeSpan.FromSeconds(10),
TransientStatusCodes = GrpcTransientErrors.StatusCodes.ToHashSet(),
};
var channel = GrpcChannel.ForAddress("https://my-service:5001",
new GrpcChannelOptions
{
Interceptors = { new PollyClientInterceptor(options) }
});
// All calls through this channel are automatically protected
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "world" });// Program.cs
builder.Services.AddPollyGrpc("https://my-service:5001", pipeline =>
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = GrpcTransientErrors.IsTransient,
})
.AddTimeout(TimeSpan.FromSeconds(10))
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
MinimumThroughput = 10,
SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(15),
}));
// Service
public class GreeterService(ResilientGrpcChannel resilient, GrpcChannel channel)
{
private readonly Greeter.GreeterClient _client = new(channel);
public Task<HelloReply> SayHelloAsync(string name, CancellationToken ct = default) =>
resilient.ExecuteAsync(token =>
_client.SayHelloAsync(new HelloRequest { Name = name }, cancellationToken: token), ct);
}| Method | Description |
|---|---|
ExecuteAsync<T>(Func<CancellationToken, AsyncUnaryCall<T>>) |
Unary call — automatically disposes the call handle |
ExecuteAsync<T>(Func<CancellationToken, Task<T>>) |
Any async operation returning T |
ExecuteAsync(Func<CancellationToken, Task>) |
Any async operation with no return value |
[Timeout] → [Retry] → [Circuit Breaker] → [gRPC server]
pipeline
.AddTimeout(TimeSpan.FromSeconds(10)) // 1. Per-attempt deadline
.AddRetry(retryOptions) // 2. Retry transient failures
.AddCircuitBreaker(cbOptions) // 3. Open circuit under load| Package | Downloads | Description |
|---|---|---|
| PollyHealthChecks | ASP.NET Core health checks for Polly v8 circuit breakers — expose circuit-breaker state (Closed, HalfOpen, Open, Isolated) as /health endpoint responses | |
| PollyBackoff | Backoff delay strategies for Polly v8 resilience pipelines | |
| PollyEFCore | Polly v8 resilience pipelines for Entity Framework Core — wrap every EF Core query and SaveChanges with retry, timeout and circuit-breaker via a single AddPollyResilience() call | |
| PollyRabbitMQ | Polly v8 resilience for RabbitMQ.Client v7+ — retry, circuit-breaker, and timeout for IChannel operations, with built-in RabbitMqTransientErrors predicate covering AlreadyClosedException, BrokerUnreachableException, OperationInterruptedException, and ConnectFailureException | |
| PollyMailKit | Polly v8 resilience pipelines for MailKit — retry, timeout, and circuit-breaker for SmtpClient.SendAsync and any MailKit SMTP operation | |
| PollyMassTransit | Polly v8 resilience pipelines for MassTransit — retry, timeout, and circuit-breaker for IBus.Publish and ISendEndpointProvider.Send | |
| PollyNpgsql | Polly v8 resilience pipelines for Npgsql (PostgreSQL) — retry, timeout, and circuit-breaker for NpgsqlConnection queries and commands, plus a built-in PostgresTransientErrors predicate covering all common PostgreSQL transient SQLSTATE codes | |
| PollyOpenAI | Polly v8 resilience for OpenAI and Azure OpenAI API calls | |
| PollyAzureEventHub | Polly v8 resilience pipelines for Azure Event Hubs — retry, timeout, and circuit-breaker for EventHubProducerClient and EventHubConsumerClient | |
| PollyElasticsearch | Polly v8 resilience pipelines for Elastic.Clients.Elasticsearch 8+ — retry, timeout, and circuit-breaker for any Elasticsearch operation, plus a built-in ElasticTransientErrors predicate covering rate limiting (429), service unavailability (503), gateway timeouts (504), and connection failures | |
| PollyHangfire | Polly v8 resilience pipelines for Hangfire — retry, timeout, and circuit-breaker for IBackgroundJobClient.Enqueue and Schedule | |
| PollyCosmosDb | Polly v8 resilience pipelines for Azure Cosmos DB — retry, timeout, and circuit-breaker for Container operations, plus a built-in CosmosTransientErrors predicate covering rate limiting (429), timeouts (408), partition failovers (410), and service unavailability (503) | |
| PollySendGrid | Polly v8 resilience pipelines for SendGrid — retry, timeout, and circuit-breaker for ISendGridClient.SendEmailAsync | |
| PollyMongo | Polly v8 resilience pipelines for MongoDB.Driver — wrap Find, InsertOne, UpdateOne, DeleteOne and other IMongoCollection calls with retry, timeout, circuit-breaker, and more using a single ResilientMongoCollection decorator | |
| PollyDapper | Polly v8 resilience pipelines for Dapper — wrap QueryAsync, ExecuteAsync, and other Dapper calls with retry, timeout, circuit-breaker, and more using a single ResilientDbConnection decorator | |
| PollyMediatR | Polly v8 resilience pipelines for MediatR — add retry, timeout, circuit-breaker, rate-limiting, hedging, and chaos engineering to any MediatR request handler with a single line of DI registration | |
| PollySqlClient | Polly v8 resilience pipelines for Microsoft.Data.SqlClient (SQL Server and Azure SQL) — retry, timeout, and circuit-breaker for SqlConnection queries and commands, plus a built-in SqlServerTransientErrors predicate covering all common SQL Server and Azure SQL transient error numbers | |
| PollyAzureKeyVault | Polly v8 resilience pipelines for Azure Key Vault — retry, timeout, and circuit-breaker for SecretClient, KeyClient, and CertificateClient | |
| PollyAzureQueueStorage | Polly v8 resilience pipelines for Azure Queue Storage — retry, timeout, and circuit-breaker for Azure.Storage.Queues QueueClient | |
| PollyRedis | Polly v8 resilience for StackExchange.Redis | |
| PollyAzureServiceBus | Polly v8 resilience for Azure Service Bus — retry, circuit breaker, and timeout for sending and receiving messages | |
| PollyAzureBlob | Polly v8 resilience pipelines for Azure Blob Storage — wrap BlobClient and BlobContainerClient operations with retry, timeout, circuit-breaker, and more using ResilientBlobClient and ResilientBlobContainerClient decorators | |
| PollyAzureTableStorage | Polly v8 resilience pipelines for Azure Table Storage — retry, timeout, and circuit-breaker for Azure.Data.Tables TableClient |
The author of this package is available for consulting on Polly v8 resilience, Azure cloud architecture, and clean .NET design.
→ solidqualitysolutions.com · LinkedIn
MIT