gRPC ASP.NET Core — Silent Leak from Missing Deadline
Without a deadline, gRPC calls hold connections indefinitely, exhausting the client pool after a few hours — leading to DEADLINE_EXCEEDED errors..
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
- gRPC uses Protocol Buffers for binary, strongly-typed contracts
- ASP.NET Core adds first-class gRPC support via Grpc.AspNetCore
- Unary, server-streaming, client-streaming, and bidirectional streaming are the four RPC types
- Interceptors act like middleware — they inspect and modify calls at the server or client
- Production traps: deadlines not set, load balancers that don't support HTTP/2, and browser limitations
- Performance: Protobuf serialization is 3–10x faster and 3–10x smaller than JSON
Imagine two chefs in different restaurants trying to share a recipe. Instead of texting paragraphs back and forth (REST/JSON), they agree on a printed recipe card template upfront — every blank is numbered, every measurement is exact, no ambiguity. gRPC is that pre-agreed recipe card. Both sides know exactly what goes where before a single byte travels the wire, so communication is lightning-fast and impossible to misinterpret. The recipe card format is called a .proto file, and it's the contract both sides sign before the conversation even starts.
Microservices are eating the enterprise, and with that comes a brutal problem: how do dozens of services talk to each other efficiently, reliably, and without turning into a web of hand-rolled HTTP clients? REST and JSON got us far, but they carry hidden costs — verbose payloads, no enforced contracts, no built-in streaming, and type information that evaporates at the boundary. At scale, these costs compound. A single inter-service call that serializes a 50-field object as JSON a million times a day quietly burns CPU, bandwidth, and developer sanity.
gRPC was built by Google to solve exactly this. It puts a strongly-typed contract — the .proto file — at the center of every conversation, uses Protocol Buffers for binary serialization that's 3–10x smaller than equivalent JSON, and rides HTTP/2 so you get multiplexing, header compression, and real bidirectional streaming for free. ASP.NET Core embraced gRPC as a first-class citizen starting with .NET 3.0, meaning you get Kestrel's raw performance, the full DI ecosystem, middleware, and health checks all wired up without ceremony.
By the end of this article you'll know how Protobuf encodes data on the wire, how to build all four gRPC communication patterns in ASP.NET Core (unary, server-streaming, client-streaming, and bidirectional), how to write interceptors that behave like middleware, and exactly which production traps — deadlines, load balancing, browser compatibility — will catch you off-guard if you don't know they're there.
What is gRPC and Why ASP.NET Core?
gRPC is a high-performance RPC framework by Google that uses Protocol Buffers for serialization and HTTP/2 for transport. ASP.NET Core provides built-in support via the Grpc.AspNetCore package, which integrates with Kestrel's HTTP/2 pipeline. Unlike REST, where you craft endpoints manually, gRPC generates client and server stubs from a shared .proto contract. This means both sides speak the same typed language — no guessing field names, no parsing JSON strings for every call. ASP.NET Core adds dependency injection, logging, and middleware (interceptors) out of the box, making it a natural home for gRPC in the .NET ecosystem.
- REST: each request carries its own schema (JSON). The server parses and validates every time.
- gRPC: the schema (proto) is agreed before the first call. Serialization is a binary protocol — fast and deterministic.
- This difference matters when you have 1000+ services exchanging billions of messages per day.
Protobuf Wire Format and Code Generation
Protocol Buffers (Protobuf) are the serialization engine of gRPC. Messages are defined in .proto files, then compiled into C# classes by the protoc compiler or the Grpc.Tools NuGet package. The on-the-wire format is a compact binary representation: field tags, wire types, and values. Varint encoding keeps small integers small, and fields can be omitted entirely if not set. This results in payloads often 3–10x smaller than equivalent JSON.
The code generator produces both the message classes (with Equals/GetHashCode overrides, ToString, and Clone) and the service stubs (client proxy and server base). In ASP.NET Core, you reference the generated files from your .csproj and implement the server base class. The generated client handles serialization, HTTP/2 framing, and connection pooling.
Building All Four gRPC Streaming Patterns in ASP.NET Core
gRPC defines four RPC types: unary (one request, one response), server-streaming (one request, multiple responses), client-streaming (multiple requests, one response), and bidirectional streaming (multiple requests, multiple responses). ASP.NET Core supports all four with simple async method signatures.
Unary is the most common; streaming is where gRPC shines for real-time data. Server-streaming is ideal for watch operations (e.g., monitoring stream). Client-streaming is useful for uploads or batch processing. Bidirectional streaming enables chat, live collaboration, or game state sync.
Here's a server-streaming example: a service that returns stock price updates.
throw an RpcException with StatusCode.Cancelled when cancelled. Not respecting cancellation leads to resource leaks.ObjectDisposedException. Always WriteAsync with CancellationToken and check it inside loops.WriteAsync and loop conditions.Interceptors — gRPC Middleware in ASP.NET Core
Interceptors are the gRPC analogue of ASP.NET Core middleware. They run before and after each RPC call on both the server and client side. Use them for cross-cutting concerns like logging, authentication, rate limiting, and metrics.
Server-side interceptors implement Interceptor and override UnaryServerHandler, ServerStreamingServerHandler, etc. Client-side interceptors work similarly. They are registered via DI and can be ordered.
A common pattern is a server interceptor that validates the JWT token from the metadata and sets a user identity for the handler.
- Middleware: can modify raw HTTP/2 frames, authentication, compression. Use for global HTTP concerns.
- Interceptors: have access to the deserialized request/response objects and metadata headers. Use for gRPC-specific logic.
- Recommendation: use middleware for authentication and rate limiting; use interceptors for business context enrichment and logging.
Production Gotchas: Deadlines, Load Balancing, and Browser Compatibility
gRPC relies on HTTP/2, which brings several production pitfalls.
- Deadlines: gRPC calls can hang forever if no deadline is set. Always set a client-side deadline (
Deadline) and respectCancellationTokenon the server. - Load Balancing: L7 load balancers need to support HTTP/2 connection multiplexing. Many legacy balancers (e.g., AWS Classic LB) don't — they break gRPC. Use Envoy, Nginx with HTTP/2, or a gRPC-aware balancer. Also, gRPC connections are typically long-lived; round-robin load balancing across connections may not evenly distribute requests. Use client-side load balancing or a service mesh.
- Browser Compatibility: gRPC-Web or gRPC JSON transcoding needed for browser clients. ASP.NET Core supports gRPC-Web via
app..UseGrpcWeb() - Health Checks: Built-in gRPC health check protocol (
grpc.health.v1.Health) enables liveness and readiness probes without custom endpoints.
gRPC Client Setup: Don't Let NuGet Eat Your Weekend
You've built the server. Now the client needs to talk to it without dying at 2 AM. The official templates hide the wiring, but production doesn't forgive magic. You need the right packages, the right proto reference, and zero assumptions about network topology. The client is where most timeouts and serialization bugs surface. Here's the blueprint for a console client that won't betray you. Start with Grpc.Net.Client for the core, Google.Protobuf for message deserialization, and Grpc.Tools for code generation. Never use the full framework unless you need gRPC-Web or special auth. The proto file must be copied and linked — not just referenced — or your builds will fail silently when the contract changes. Set the channel address from configuration, not a string literal. In production, that address changes faster than your sprint goals.
GrpcServices="Client" in your protobuf item group. A missing attribute compiles server-side code into your client, bloating it and risking runtime crashes.The Greeter Client: One Call, One Shot, No Second Chances
Now you have the packages. Now you write the code that actually calls the service. This is where theory meets the debugger. The Greeter service expects a HelloRequest with a name. Your client sends it and expects a HelloReply. Straightforward? Yes, until the channel is dead or the deadline expired. Create a GrpcChannel pointing at your server's HTTPS endpoint. Use insecure only in dev, and never on a shared network. Build the GreeterClient from that channel. Wrap the call in a try-catch for RpcException. gRPC throws on network failures, not nulls. The async call awaits the response. A successful reply returns the Message field. Anything else is a failure you log, then fix. Never swallow exceptions in production — your on-call phone will ring.
Deadline Not Propagated — The Silent Connection Leak
var call = client.SomeMethod(request); without a cancellation token or deadline.var call = client.SomeMethod(request, deadline: DateTime.UtcNow.AddSeconds(5)); Use CancellationToken for cancellation. On the server side, enforce a maximum deadline via CallContext.Deadline or middleware.- Always set a deadline on every gRPC call, even local ones.
- Test connection recovery by killing the server and observing client behaviour.
- Monitor gRPC metrics: pending calls, call duration, and connection count.
grpc.invoke.latency metrics.app.UseGrpcWeb(); or set GrpcChannelOptions.ThrowOperationCanceledOnCancellation = true. Check server-side exception logs.GrpcChannel.MaxRetryBufferSize and ensure deadlines are propagated. Use CallContext.Deadline to enforce server-side timeout.dotnet-counters monitor --process-id $(pid) Grpc.AspNetCore.Server.TotalCallskubectl logs -l app=my-grpc-service --tail=100 | grep -i deadlineKey takeaways
Common mistakes to avoid
4 patternsNot setting a deadline on gRPC calls
Deadline (e.g., DateTime.UtcNow.AddSeconds(5)) on each call. Use CancellationToken for cancellation.Using stream without cancellation
ObjectDisposedException and log noise.context.CancellationToken in streaming loops and pass it to WriteAsync. Throw RpcException on cancellation.Deploying behind an HTTP/1.1 load balancer
HttpProtocols.Http2 in Kestrel.Ignoring gRPC-Web for browser clients
app.UseGrpcWeb() and annotate services with .EnableGrpcWeb(). Use the grpc-web JavaScript client.Interview Questions on This Topic
Explain the difference between gRPC unary and streaming calls. When would you use each?
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
That's ASP.NET. Mark it forged?
4 min read · try the examples if you haven't