Skip to content

gRPC Advanced

Key Points

  • Interceptors = gRPC's middleware. Server-side and client-side. Cross-cutting (logging, auth, retry, metrics).
  • Deadlines vs cancellation tokens: deadline propagates across services automatically; CancellationToken local. Use both.
  • Retries with config-based policy: built into the gRPC channel; declarative; exponential backoff.
  • Headers/Metadata: Metadata collection; auth tokens, correlation IDs, tracing — sent via headers.
  • gRPC-Web vs Connect protocol: Connect is a newer cross-protocol successor. gRPC-Web is the broader-supported browser option.
  • Bidirectional streaming patterns: chat, telemetry, real-time price feeds. Backpressure via WriteAsync returning Task.

Concepts (deep dive)

Server interceptors

public class AuthInterceptor : Interceptor
{
    public override async Task<TResp> UnaryServerHandler<TReq, TResp>(
        TReq request, ServerCallContext ctx, UnaryServerMethod<TReq, TResp> continuation)
    {
        var token = ctx.RequestHeaders.GetValue("authorization");
        if (string.IsNullOrEmpty(token))
            throw new RpcException(new Status(StatusCode.Unauthenticated, "Missing token"));
        return await continuation(request, ctx);
    }
}

builder.Services.AddGrpc(o => o.Interceptors.Add<AuthInterceptor>());

Server intercepts all four call types via overrides: UnaryServerHandler, ClientStreamingServerHandler, ServerStreamingServerHandler, DuplexStreamingServerHandler.

Client interceptors

public class RetryClientInterceptor : Interceptor { /* ... */ }

builder.Services.AddGrpcClient<Greeter.GreeterClient>(o => o.Address = uri)
    .AddInterceptor<RetryClientInterceptor>();

Deadline propagation

Caller A: deadline 5s → calls Service B (with deadline header)
Service B: receives deadline; uses ctx.CancellationToken (which fires at deadline)
Service B: calls Service C, propagating remaining deadline

Built into gRPC. Don't reset the deadline on chained calls.

Built-in retry policy

{
  "methodConfig": [
    {
      "name": [{ "service": "Greeter" }],
      "retryPolicy": {
        "maxAttempts": 5,
        "initialBackoff": "1s",
        "maxBackoff": "5s",
        "backoffMultiplier": 1.5,
        "retryableStatusCodes": [ "UNAVAILABLE" ]
      }
    }
  ]
}

Configured via ServiceConfig:

var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions
{
    ServiceConfig = JsonSerializer.Deserialize<ServiceConfig>(serviceConfigJson)
});

Declarative; works without Polly.

For richer retry, use Polly via standard resilience handler:

.AddStandardResilienceHandler();

Hedging policy (for redundancy)

"hedgingPolicy": {
  "maxAttempts": 3,
  "hedgingDelay": "0.1s",
  "nonFatalStatusCodes": [ "UNAVAILABLE" ]
}

Sends parallel attempts; first success wins.

Headers / Metadata

var metadata = new Metadata
{
    { "authorization", $"Bearer {token}" },
    { "x-trace-id", traceId },
    { "x-binary-data-bin", payloadBytes }   // -bin suffix = binary header
};
await client.SayHelloAsync(req, metadata);
// Server reads:
var token = ctx.RequestHeaders.GetValue("authorization");

// Server writes response headers:
await ctx.WriteResponseHeadersAsync(new Metadata { { "x-version", "1.0" } });

// Server writes trailers (after response):
ctx.ResponseTrailers.Add("x-completed-at", DateTime.UtcNow.ToString("o"));

Connect protocol (alternative)

Connect by Buf: HTTP/1.1 friendly; supports gRPC, gRPC-Web, and Connect's own JSON protocol. Single server can serve all three.

// Connect in .NET via 3rd-party packages (smaller ecosystem than gRPC native)

For 2026, gRPC + gRPC-Web still dominates .NET. Connect is gaining momentum elsewhere.

Compression

builder.Services.AddGrpc(o =>
{
    o.ResponseCompressionLevel = CompressionLevel.Optimal;
    o.ResponseCompressionAlgorithm = "gzip";
});

// Client requests compression:
.ConfigureChannel(c => c.CompressionProviders = new() { new GzipCompressionProvider() });

For large payloads (>1KB), compression saves bandwidth significantly.

Authorization integration

[Authorize(Policy = "RequireAdmin")]
public class AdminService : Admin.AdminBase
{
    [Authorize(Policy = "Permission:Users.Read")]
    public override Task<UsersReply> ListUsers(...) { /* ... */ }
}

Same authorization as ASP.NET MVC controllers.

Health check

gRPC has a standard health check protocol:

builder.Services.AddGrpcHealthChecks()
    .AddCheck("liveness", () => HealthCheckResult.Healthy());
app.MapGrpcHealthChecksService();

Clients can call Health/Check to test service health.

Reflection (development)

builder.Services.AddGrpcReflection();
app.MapGrpcReflectionService();

Lets tools (grpcurl, Postman) discover services without .proto files. Disable in prod.

Channel options

new GrpcChannelOptions
{
    MaxReceiveMessageSize = 4 * 1024 * 1024,
    MaxSendMessageSize = 4 * 1024 * 1024,
    ThrowOperationCanceledOnCancellation = true,
    HttpHandler = new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,
        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
    }
}

EnableMultipleHttp2Connections lets concurrent calls span connections (default is one).

Streaming patterns

Client streaming (upload)

public override async Task<Summary> Upload(IAsyncStreamReader<Chunk> reader, ServerCallContext ctx)
{
    long bytes = 0;
    await foreach (var chunk in reader.ReadAllAsync())
        bytes += chunk.Data.Length;
    return new Summary { TotalBytes = bytes };
}

Bi-directional (chat)

public override async Task Chat(IAsyncStreamReader<Msg> reqs, IServerStreamWriter<Msg> writer, ServerCallContext ctx)
{
    await foreach (var msg in reqs.ReadAllAsync())
        await writer.WriteAsync(new Msg { Text = $"echo: {msg.Text}" });
}

Backpressure in streaming

writer.WriteAsync returns Task — if client is slow, awaits. Don't write unbounded.

Load balancing

Native gRPC client load balancing across multiple backend addresses:

var channel = GrpcChannel.ForAddress("dns:///myservice", new GrpcChannelOptions
{
    Credentials = ChannelCredentials.SecureSsl,
    ServiceConfig = new ServiceConfig
    {
        LoadBalancingConfigs = { new RoundRobinConfig() }
    }
});

Or use a service mesh / YARP for L7 LB.

gRPC over Unix sockets

new GrpcChannelOptions
{
    HttpHandler = new SocketsHttpHandler
    {
        ConnectCallback = async (ctx, ct) =>
        {
            var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            await socket.ConnectAsync(new UnixDomainSocketEndPoint("/tmp/sock"));
            return new NetworkStream(socket, ownsSocket: true);
        }
    }
}

For sidecar / co-located services. Lower latency than TCP loopback.


Code: correct vs wrong

❌ Wrong: cross-cutting in every method

public override async Task<X> Method(...)
{
    LogStart(); try { ... } finally { LogEnd(); }
}

✅ Correct: interceptor

public class LoggingInterceptor : Interceptor { /* once */ }

❌ Wrong: ignoring deadlines

public override async Task<X> Method(X req, ServerCallContext ctx)
{
    var data = await db.LongQuery();   // no token; might run after deadline
}

✅ Correct: pass token

var data = await db.LongQuery(ctx.CancellationToken);

Design patterns for this topic

Pattern 1 — "Interceptors for cross-cutting"

  • Intent: logging, auth, retry per call type.

Pattern 2 — "Deadline propagation across hops"

  • Intent: end-to-end timeout.

Pattern 3 — "ServiceConfig retry"

  • Intent: declarative; no Polly needed for simple cases.

Pattern 4 — "Compression for large payloads"

  • Intent: bandwidth.

Pattern 5 — "Reflection in dev only"

  • Intent: tooling support; secure prod.

Pros & cons / trade-offs

Feature Pros Cons
Interceptors Cross-cutting Each call type override
ServiceConfig retry Declarative Limited to server config
Polly resilience Rich Layered
Hedging Tail latency reduction More traffic

When to use / when to avoid

  • Use interceptors for any cross-cutting.
  • Use deadlines on every call.
  • Use ServiceConfig retry for simple cases.
  • Avoid custom retry-on-everything — match status codes.

Interview Q&A

Q1. Server interceptor purpose? Cross-cutting concerns: logging, auth, metrics, error handling — applied to all gRPC calls.

Q2. Deadline vs CancellationToken? Deadline: propagated across service hops via gRPC. Token: in-process. Both used; deadline triggers token.

Q3. ServiceConfig retry? Declarative retry config in JSON; built into gRPC channel. No Polly required for simple.

Q4. Hedging policy? Parallel attempts; first success wins. Reduces tail latency.

Q5. Metadata / headers? Custom headers via Metadata collection. -bin suffix for binary headers.

Q6. gRPC reflection? Lets clients discover services without .proto. Dev convenience; disable in prod.

Q7. Connect protocol? Newer cross-protocol (HTTP/1, gRPC, gRPC-Web). Smaller .NET ecosystem; gaining elsewhere.

Q8. Compression? gzip via channel/server options. Big win for large payloads.

Q9. Health check? Standard gRPC health protocol. AddGrpcHealthChecks + reflective endpoint.

Q10. Backpressure in streaming? writer.WriteAsync returns Task. Awaits when buffer full.

Q11. LB at gRPC client? ServiceConfig + DNS resolver. Or service mesh.

Q12. Unix domain sockets? ConnectCallback in SocketsHttpHandler. Low-latency intra-host.


Gotchas / common mistakes

  • ⚠️ No deadline propagation — hangs across service chain.
  • ⚠️ Reflection in prod — info leak.
  • ⚠️ Default 4MB message limit — large payloads fail silently.
  • ⚠️ No interceptor for auth — duplicated logic.
  • ⚠️ Retry on non-idempotent — duplicates.

Further reading