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:
Metadatacollection; 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
WriteAsyncreturning 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:
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.
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)
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
✅ Correct: interceptor
❌ 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
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.