gRPC Performance: Why You Should Reuse a gRPC Channel
1. What Is a gRPC Channel?
When your application (the client) wants to call a remote function on another application (the server), it first needs to establish a communication path between the two. In gRPC, this communication path is called a channel.
Think of a gRPC channel like a phone call. Before you can speak to someone, you need to dial their number, wait for them to pick up, and exchange some pleasantries to confirm you are both ready to talk. Only then can real conversation happen. The channel is exactly that: the established, ready-to-use connection between client and server.
In .NET, the channel is represented by the GrpcChannel class from the Grpc.Net.Client namespace:
Once the channel is open, you can create a gRPC client from it and start making calls:
2. What Happens When You Open a Channel?
Opening a channel is not free. Under the hood, several expensive steps happen:
- A socket is opened — The operating system allocates a network socket, which is a low-level resource used for communication.
- A TCP connection is established — TCP is the reliable transport protocol used to send bytes across the internet. Establishing it requires multiple round trips (the "TCP handshake").
- TLS is negotiated — Because gRPC uses HTTP/2, it typically runs over HTTPS. TLS (Transport Layer Security) is a cryptographic protocol that ensures your data is private and tamper-proof. Negotiating it requires more round trips.
- An HTTP/2 connection is started — HTTP/2 is the protocol that gRPC messages travel over. Setting it up also requires handshaking.
All of these steps involve sending packets back and forth between the client and server over the network. Even on a fast local network, this takes time — typically several milliseconds. Over a real internet connection, it can be tens or even hundreds of milliseconds.
Real-world analogy: Imagine you need to ask your colleague 1,000 questions. You have two options: (A) walk to their desk once, sit down, and ask all 1,000 questions in one sitting, or (B) walk to their desk, ask one question, go back to your seat, walk again for the next question, and repeat 1,000 times. Option B is obviously ridiculous — but that's exactly what happens when you create a new channel for every gRPC call.
3. The Costly Mistake: Creating a New Channel Every Time
Beginners often write code like this:
What is happening here? For every single call in the loop:
- A new socket is opened
- A new TCP connection is established
- TLS is negotiated again from scratch
- HTTP/2 is re-initialized
- Then the actual gRPC call happens
- Then the channel is disposed (connection closed)
This is enormously wasteful. Instead of spending time doing useful work, most of the time is spent on connection setup and teardown. In practice, this can make your application 2x to 10x slower than it needs to be.
4. Channel vs. Client: What Should You Reuse?
This is a very common point of confusion. gRPC in .NET has two objects you work with:
GrpcChannel— The actual network connection. This is the expensive object. You must reuse this.Monitor.MonitorClient(or any generated client class) — A thin wrapper around the channel. It provides strongly-typed C# methods that correspond to your.protoRPC definitions. This is cheap to create.
The generated client does not do any heavy network work itself. All the heavy lifting — managing the connection, sending bytes, receiving bytes — is done by the underlying GrpcChannel. The client is just a convenient API layer on top of the channel.
This means:
- ✅ Safe to create a new client object per call — it's cheap and thread-safe to do so.
- ❌ Do NOT create a new channel per call — it's expensive and defeats the purpose of gRPC.
5. Bad Example: New Channel Per Call
Let's look at a complete, runnable bad example. Imagine you have a gRPC server with a Monitor service and you want to call it 100 times from an API endpoint.
The problem is the using var channel = GrpcChannel.ForAddress(serverUrl) line inside the loop. Every iteration opens and closes a full network connection. With 1,000 calls, this took approximately 25 seconds in tests.
6. Good Example: Reuse the Channel
The fix is straightforward: create the channel once and reuse it for all calls. Here is a proper wrapper class that does this:
And the controller endpoint that uses this wrapper:
With 1,000 calls, this version took approximately 15 seconds — 40% faster than the bad example, just by keeping the channel open.
7. Best Practice: Register as Singleton
In a real ASP.NET Core application, the correct way to manage the channel lifetime is to register your wrapper class as a singleton in the dependency injection container. A singleton lives for the entire lifetime of the application, which means the channel is created once at startup and reused by every request.
Now any controller or service that needs to make gRPC calls simply asks for IGrpcPerformanceClient via constructor injection, and the framework provides the same singleton instance every time:
Why singleton? Because a transient registration (default for services) would create a new wrapper instance — and therefore a new channel — for every HTTP request that comes in. That is nearly as bad as creating a new channel per gRPC call.
8. Performance Comparison
Here is a summary of the three approaches and their measured performance when making 1,000 sequential gRPC calls:
| Approach | Channel Reused? | Time (1,000 calls) |
|---|---|---|
New channel per call (initialized-client) | No | ~25 seconds |
Framework factory client (AddGrpcClient) | Yes, but framework-controlled | ~50 seconds |
Custom wrapper with shared channel (client-wrapper) | Yes, fully controlled | ~15 seconds |
The surprising result is that the framework-provided factory client (registered via AddGrpcClient) actually performed worst, despite reusing a channel. The likely reason is that the framework applies additional configuration and overhead that is not optimized for high-frequency sequential calls.
The lesson is: when performance matters, control your own channel creation. Do not blindly trust the framework to do it optimally for your specific use case.
9. Summary
- A gRPC channel is an established network connection (socket + TCP + TLS + HTTP/2). It is expensive to create.
- Creating a new channel per call forces all that expensive setup to happen on every single call. This is a major performance mistake.
- The gRPC client object (e.g.,
Monitor.MonitorClient) is just a thin wrapper. It is safe and cheap to create per call. The channel underneath is what matters. - Reuse the channel by storing it as a singleton — either in a custom wrapper class or by ensuring your service that holds it is registered as a singleton in ASP.NET Core's DI container.
- Even though
AddGrpcClientis convenient, it may not give you the best performance. For maximum control and speed, wrap the channel yourself.