Skip to content

gRPC

A policy manages retries, timeouts, and circuit breaking for any call. gRPC introduces its own constraints, and they are not the same ones HTTP has.

Register the interceptor on the builder that AddGrpcClient<T>() returns:

csharp
services.AddGrpcClient<OrdersClient>(o => o.Address = new Uri("https://orders.internal:5001"))
    .AddGrpcResilience();

That client now makes three attempts with exponential backoff, retries an Unavailable but not a NotFound, opens a circuit breaker per gRPC service, and tells the server how long each attempt has. For more information about status codes, see Classification.

IMPORTANT

AddResilience() also compiles on a gRPC client builder, and it does nothing useful. Every gRPC call is an HTTP POST, which the resilience handler refuses to retry by default, and a gRPC failure travels in the grpc-status trailer on an HTTP 200, which the HTTP classifier reads as a success. Use AddGrpcResilience().

Install

bash
dotnet add package NResilience.Grpc

The package depends on NResilience, NResilience.Extensions, Grpc.Core.Api, and Grpc.Net.ClientFactory. A gRPC client's dependency graph already contains most of that weight.

Interceptor capabilities

ResilienceInterceptor runs a policy around each gRPC call:

  • Status classification: Reads the StatusCode on an RpcException, which is where a gRPC failure lives. See Classification.
  • Repeatable by default: Retries unary calls unless you say otherwise - the opposite of the HTTP default. See Idempotency.
  • Attempt deadline propagation: Writes each attempt's ceiling into CallOptions.Deadline, which grpc-dotnet sends as the standard grpc-timeout header. See Deadlines.
  • Per-service scoping: Scopes the circuit breaker, the retry budget, and the hedging latency estimate to the gRPC service. See Per-service scope.
  • Nested retry detection: Reports when retries are happening in layers, under the same marker the HTTP handler uses. See Nested retries.
  • Server streaming: Retries a server stream until its first message, and hands the rest of the enumeration over untouched. See Streaming.
  • Call management: Disposes the gRPC calls that a retry supersedes.

Configure the interceptor

Pass a policy, options, or both:

csharp
services.AddGrpcClient<OrdersClient>(o => o.Address = new Uri("https://orders.internal:5001"))
    .AddGrpcResilience(
        GrpcResilience.Default with { Attempts = 4 },
        o =>
        {
            // A charge must not be repeated, whatever the transport says.
            o.IsRepeatable = static method => method.Name != "ChargeCard";

            // One breaker per method rather than per service.
            o.ScopeBy = static method => method.FullName;
        });
OptionDefaultDescriptionReference
IsRepeatableevery methodDecides whether a method may be repeated.Idempotency
ScopeBym => m.ServiceNameThe breaker, budget, and latency-window scope key. null is one scope per client.Per-service scope
MaxScopes1024Bounds the scope registry.Per-service scope
BreakerPerScopetrueGives each scope its own circuit breaker.Per-service scope
BreakerSettingsnullThe settings those breakers are built with.Breaker
PropagateAttemptDeadlinetrueWrites the attempt ceiling into CallOptions.Deadline.Deadlines
DeadlineSlack50 msHow much longer than the ceiling that deadline is set.Deadlines
OwnTransportTimeouttrueSets HttpClient.Timeout to infinite so it stops competing with the deadline.Deadlines
DetectNestedRetriestrueStamps and reads the nested-retry marker.Nested retries

Register the interceptor first

Register AddGrpcResilience() before any other interceptor. Interceptors registered after it run per attempt, which is where an interceptor that refreshes a token wants to be - a token fetched once outside the retry loop can expire during it.

The gRPC client factory does not expose the registrations already made, so the order is a rule rather than something the library can enforce.

Which calls are wrapped

Server-streaming calls are wrapped on the core library's streaming semantic: retried until their first message, and never after it. The one thing that differs from a unary call is the deadline on the wire, which for a stream is the whole call's remaining budget. See Streaming.

Client-streaming and duplex calls pass through untouched. The request stream is a source you drive interactively, and repeating one means re-enumerating something the failed attempt has already partially consumed, which produces duplicates or requires buffering everything. Neither outcome is a resilience feature. Wrap the setup call instead, the way any other callback is wrapped.

The synchronous BlockingUnaryCall throws a NotSupportedException. Passing it through silently would leave one call in the client with no retry, no breaker, and no deadline. Use the generated client's Async overload.

Read what it holds

csharp
// One breaker and one budget per gRPC service by default, keyed by the service's full name -
// so an operator can be told which dependency opened, not merely that something did.
foreach (var (service, breaker) in interceptor.Breakers())
    Console.WriteLine($"{service}: {breaker.State}");

The registration also adds these to ResilienceHealthOptions, so a health endpoint reports them without any wiring of yours.

Released under the MIT License.