| name | grpc-resilience |
| description | gRPC resilience patterns — retry policies, hedging, deadlines, cancellation, and transient fault handling for .NET gRPC clients. Use when implementing fault-tolerant gRPC communication. |
gRPC Resilience Patterns
Retry Policy
Automatic retry for failed gRPC calls. Built into Grpc.Net.Client (v2.36.0+).
Configuration
var defaultMethodConfig = new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 5,
InitialBackoff = TimeSpan.FromSeconds(1),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 1.5,
RetryableStatusCodes = { StatusCode.Unavailable }
}
};
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }
});
Retry Options
| Option | Description |
|---|
MaxAttempts | Total attempts including the original (min 2, capped by GrpcChannelOptions.MaxRetryAttempts default 5) |
InitialBackoff | Initial delay between retries (must be > 0) |
MaxBackoff | Upper limit for exponential backoff growth |
BackoffMultiplier | Multiplier applied to backoff after each attempt |
RetryableStatusCodes | Status codes that trigger a retry (at least one required) |
Common Retryable Status Codes
StatusCode.Unavailable — service temporarily down
StatusCode.DeadlineExceeded — timeout (use cautiously)
StatusCode.Aborted — transaction conflict
StatusCode.ResourceExhausted — rate limited (with backoff)
When Retries Are Valid
Calls are retried when ALL conditions are met:
- Status code matches
RetryableStatusCodes
- Previous attempts <
MaxAttempts
- Call is NOT committed (no response headers received, buffer not exceeded)
- Deadline not exceeded
Retry Detection
The server can detect retries via grpc-previous-rpc-attempts metadata:
public override Task<Response> MyMethod(Request request, ServerCallContext context)
{
var retryAttempt = context.RequestHeaders
.FirstOrDefault(h => h.Key == "grpc-previous-rpc-attempts")?.Value;
}
Hedging Policy
Aggressively send multiple copies of a call without waiting for a response. First successful result wins.
var defaultMethodConfig = new MethodConfig
{
Names = { MethodName.Default },
HedgingPolicy = new HedgingPolicy
{
MaxAttempts = 5,
HedgingDelay = TimeSpan.FromMilliseconds(500),
NonFatalStatusCodes = { StatusCode.Unavailable }
}
};
Hedging vs Retry
| Aspect | Retry | Hedging |
|---|
| Execution | Sequential (wait for failure) | Parallel (send eagerly) |
| Latency | Higher (waits for each failure) | Lower (first success wins) |
| Server load | Lower | Higher (multiple calls) |
| Idempotency | Recommended | Required (multiple executions) |
| Combinable | Not with hedging | Not with retry |
Important: Hedging is only safe for idempotent operations. A hedging policy CANNOT be combined with a retry policy on the same method.
Deadlines
Always set deadlines on client calls to prevent indefinitely hanging RPCs.
var deadline = DateTime.UtcNow.AddSeconds(5);
var reply = await client.GetItemAsync(
new GetItemRequest { Id = "123" },
deadline: deadline);
var options = new CallOptions(deadline: DateTime.UtcNow.AddSeconds(10));
var reply = await client.GetItemAsync(request, options);
Server-Side Deadline Handling
public override async Task<Response> LongOperation(
Request request, ServerCallContext context)
{
if (context.CancellationToken.IsCancellationRequested)
{
throw new RpcException(new Status(StatusCode.Cancelled, "Deadline exceeded"));
}
var result = await _service.ProcessAsync(request, context.CancellationToken);
return new Response { Data = result };
}
Cancellation
Client-Side
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var reply = await client.GetItemAsync(
new GetItemRequest { Id = "123" },
cancellationToken: cts.Token);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
{
}
Server-Side
public override async Task StreamData(
Request request,
IServerStreamWriter<DataChunk> responseStream,
ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
var chunk = await GetNextChunkAsync(context.CancellationToken);
await responseStream.WriteAsync(chunk, context.CancellationToken);
}
}
Streaming Resilience
Retry Limitations with Streaming
- Server streaming / bidirectional: Retries stop after first response message is received
- Client streaming / bidirectional: Retries stop if outgoing messages exceed the buffer
- Buffer size is controlled by
MaxRetryBufferSize and MaxRetryBufferPerCallSize
Manual Reconnection Pattern
For streaming calls that cannot be auto-retried:
async Task StreamWithReconnect(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
using var call = client.ServerStreamMethod(new Request());
await foreach (var msg in call.ResponseStream.ReadAllAsync(ct))
{
ProcessMessage(msg);
}
break;
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
{
await Task.Delay(TimeSpan.FromSeconds(2), ct);
}
}
}
Transient Fault Handling Checklist
- Configure retry policies for idempotent unary calls
- Set deadlines on ALL client calls
- Use
CancellationToken in all async operations
- Handle
RpcException with status code inspection
- Implement manual reconnection for long-lived streams
- Consider hedging only for latency-critical idempotent operations
- Monitor
grpc-previous-rpc-attempts metadata on the server
- Log transient failures at
Warning level, permanent failures at Error