gRPC in .NET 10 — High-Performance Microservices Communication with Protobuf
Posted on: 4/26/2026 9:11:37 AM
Table of contents
- 1. What Is gRPC and Why Do You Need It?
- 2. Protocol Buffers — The Language of gRPC
- 3. Four gRPC Communication Patterns
- 4. Implementing a gRPC Server on .NET 10
- 5. gRPC Client in .NET 10
- 6. Interceptors — Middleware for gRPC
- 7. gRPC-Web and JSON Transcoding
- 8. gRPC vs REST vs GraphQL Comparison
- 9. Load Balancing and Resilience
- 10. Performance Optimization
- 11. Testing gRPC Services
- 12. When to Use (and Not Use) gRPC
- Conclusion
In microservices architecture, inter-service communication is the defining factor of overall system performance. REST APIs with JSON have served well for years, but as systems scale to hundreds of services, the overhead of text-based serialization and HTTP/1.1 becomes a serious bottleneck. gRPC — the RPC framework developed by Google — addresses this precisely with Protocol Buffers (binary serialization) and HTTP/2 (multiplexed connections).
This article takes a deep dive into implementing gRPC on .NET 10, from fundamentals to advanced production patterns.
1. What Is gRPC and Why Do You Need It?
gRPC (gRPC Remote Procedure Call) is an open-source, high-performance RPC framework originally developed by Google. Instead of sending JSON over HTTP/1.1 like REST, gRPC uses Protocol Buffers (Protobuf) as both its Interface Definition Language (IDL) and serialization format, transmitting data over HTTP/2.
graph LR
A[Client App] -->|Protobuf binary| B[gRPC Channel
HTTP/2]
B -->|Multiplexed streams| C[gRPC Server
.NET 10]
C -->|Contract-first| D[.proto files]
D -->|Code generation| A
D -->|Code generation| C
style A fill:#e94560,stroke:#fff,color:#fff
style B fill:#2c3e50,stroke:#fff,color:#fff
style C fill:#e94560,stroke:#fff,color:#fff
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
1.1. Core Advantages of gRPC
- Contract-first development: Define APIs in
.protofiles, auto-generate code for both server and client — ensuring end-to-end type safety. - Binary serialization (Protobuf): Payloads are 3-10x smaller than JSON, with significantly faster serialize/deserialize cycles.
- Native HTTP/2: Multiplexing multiple requests on a single connection, header compression (HPACK), and server push.
- Built-in streaming: Supports 4 communication patterns — no need for separate WebSocket or polling infrastructure.
- Deadlines and cancellation: Built-in timeout propagation across service chains.
2. Protocol Buffers — The Language of gRPC
Protobuf is the heart of gRPC. A .proto file defines both data structures (messages) and operations (services). The protoc compiler generates strongly-typed code for the target language.
syntax = "proto3";
option csharp_namespace = "OrderService.Grpc";
package orders;
// Service definition
service OrderApi {
// Unary: 1 request → 1 response
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
// Server streaming: 1 request → stream of responses
rpc WatchOrderStatus (GetOrderRequest) returns (stream OrderStatusUpdate);
// Client streaming: stream of requests → 1 response
rpc UploadOrderItems (stream OrderItem) returns (UploadSummary);
// Bidirectional streaming: stream ↔ stream
rpc OrderChat (stream ChatMessage) returns (stream ChatMessage);
}
message GetOrderRequest {
string order_id = 1;
}
message OrderResponse {
string order_id = 1;
string customer_name = 2;
repeated OrderItem items = 3;
OrderStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
PENDING = 1;
CONFIRMED = 2;
SHIPPING = 3;
DELIVERED = 4;
CANCELLED = 5;
}
message OrderItem {
string product_id = 1;
string name = 2;
int32 quantity = 3;
double price = 4;
}
💡 Best Practice
Always number fields starting from 1, and never reuse deleted field numbers — use reserved to mark them. This ensures backward compatibility as your schema evolves.
2.1. Common Protobuf Scalar Types
| Protobuf Type | C# Type | Notes |
|---|---|---|
double | double | 64-bit floating point |
float | float | 32-bit floating point |
int32 | int | Varint encoding, inefficient for negatives |
int64 | long | Varint encoding |
bool | bool | |
string | string | UTF-8 encoded |
bytes | ByteString | Arbitrary binary data |
google.protobuf.Timestamp | DateTimeOffset | Use well-known types for datetime |
3. Four gRPC Communication Patterns
The biggest advantage of gRPC over REST is native support for 4 communication patterns, all built on HTTP/2 streams.
graph TB
subgraph Unary["1. Unary RPC"]
UA[Client] -->|1 Request| UB[Server]
UB -->|1 Response| UA
end
subgraph ServerStream["2. Server Streaming"]
SA[Client] -->|1 Request| SB[Server]
SB -->|Stream Responses| SA
end
subgraph ClientStream["3. Client Streaming"]
CA[Client] -->|Stream Requests| CB[Server]
CB -->|1 Response| CA
end
subgraph BiDi["4. Bidirectional Streaming"]
BA[Client] <-->|Stream ↔ Stream| BB[Server]
end
style UA fill:#e94560,stroke:#fff,color:#fff
style UB fill:#2c3e50,stroke:#fff,color:#fff
style SA fill:#e94560,stroke:#fff,color:#fff
style SB fill:#2c3e50,stroke:#fff,color:#fff
style CA fill:#e94560,stroke:#fff,color:#fff
style CB fill:#2c3e50,stroke:#fff,color:#fff
style BA fill:#e94560,stroke:#fff,color:#fff
style BB fill:#2c3e50,stroke:#fff,color:#fff
3.1. Unary RPC — Classic Request-Response
Similar to a standard REST call: client sends 1 request, server returns 1 response. This is the most common pattern.
// Server implementation
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request, ServerCallContext context)
{
var order = await _orderRepository.GetByIdAsync(
request.OrderId, context.CancellationToken);
if (order is null)
{
throw new RpcException(new Status(
StatusCode.NotFound,
$"Order {request.OrderId} not found"));
}
return MapToResponse(order);
}
// Client call
var response = await client.GetOrderAsync(
new GetOrderRequest { OrderId = "ORD-2026-001" },
deadline: DateTime.UtcNow.AddSeconds(5));
3.2. Server Streaming — Realtime Updates
Client sends 1 request, server returns a continuous stream of responses. Ideal for realtime notifications, progress updates, or live data feeds.
// Server implementation
public override async Task WatchOrderStatus(
GetOrderRequest request,
IServerStreamWriter<OrderStatusUpdate> responseStream,
ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
var status = await _orderRepository
.GetStatusAsync(request.OrderId);
await responseStream.WriteAsync(new OrderStatusUpdate
{
OrderId = request.OrderId,
Status = status,
UpdatedAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow)
});
if (status == OrderStatus.Delivered
|| status == OrderStatus.Cancelled)
break;
await Task.Delay(2000, context.CancellationToken);
}
}
// Client consumption
using var call = client.WatchOrderStatus(
new GetOrderRequest { OrderId = "ORD-2026-001" });
await foreach (var update in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Status: {update.Status} at {update.UpdatedAt}");
}
3.3. Client Streaming — Batch Upload
Client sends a stream of data, server receives it all and returns a single summary response. Suitable for file uploads, batch processing, or telemetry ingestion.
// Server implementation
public override async Task<UploadSummary> UploadOrderItems(
IAsyncStreamReader<OrderItem> requestStream,
ServerCallContext context)
{
var items = new List<OrderItem>();
await foreach (var item in requestStream.ReadAllAsync())
{
items.Add(item);
}
var totalPrice = items.Sum(i => i.Price * i.Quantity);
return new UploadSummary
{
TotalItems = items.Count,
TotalPrice = totalPrice
};
}
3.4. Bidirectional Streaming — Full Duplex
Both client and server send independent streams over the same connection. This is the most powerful pattern, suitable for chat, collaborative editing, or multiplayer gaming.
// Server implementation
public override async Task OrderChat(
IAsyncStreamReader<ChatMessage> requestStream,
IServerStreamWriter<ChatMessage> responseStream,
ServerCallContext context)
{
await foreach (var message in requestStream.ReadAllAsync())
{
var reply = await _chatService.ProcessAsync(message);
await responseStream.WriteAsync(reply);
}
}
4. Implementing a gRPC Server on .NET 10
4.1. Project Setup
# Create project from template
dotnet new grpc -n OrderService
# Or add to existing project
dotnet add package Grpc.AspNetCore
Configuration in Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Register gRPC services
builder.Services.AddGrpc(options =>
{
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16 MB
options.MaxSendMessageSize = 16 * 1024 * 1024;
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.CompressionProviders = new List<ICompressionProvider>
{
new GzipCompressionProvider(CompressionLevel.Optimal)
};
options.ResponseCompressionAlgorithm = "gzip";
});
// Register gRPC reflection (for dev tools)
builder.Services.AddGrpcReflection();
// Register gRPC health checks
builder.Services.AddGrpcHealthChecks()
.AddCheck("order-db", new SqlHealthCheck(connectionString));
var app = builder.Build();
app.MapGrpcService<OrderApiService>();
app.MapGrpcReflectionService(); // For grpcurl, Postman, etc.
app.MapGrpcHealthChecksService();
app.Run();
4.2. Project File (.csproj)
<ItemGroup>
<Protobuf Include="Protos\orders.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.68.0" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.68.0" />
<PackageReference Include="Grpc.HealthCheck" Version="2.68.0" />
</ItemGroup>
5. gRPC Client in .NET 10
5.1. Client Factory Pattern (Recommended)
.NET provides a gRPC Client Factory integrated with HttpClientFactory, automatically managing connection pooling, lifecycle, and DI.
// Register in DI
builder.Services.AddGrpcClient<OrderApi.OrderApiClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.ConfigureChannel(channel =>
{
channel.MaxReceiveMessageSize = 16 * 1024 * 1024;
channel.ServiceConfig = new ServiceConfig
{
LoadBalancingConfigs = { new RoundRobinConfig() },
MethodConfigs =
{
new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromMilliseconds(200),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 2,
RetryableStatusCodes =
{
StatusCode.Unavailable,
StatusCode.DeadlineExceeded
}
}
}
}
};
})
.AddInterceptor<LoggingInterceptor>()
.EnableCallContextPropagation();
// Use via DI
public class OrderController(OrderApi.OrderApiClient grpcClient)
{
public async Task<OrderDto> GetOrder(string id)
{
var response = await grpcClient.GetOrderAsync(
new GetOrderRequest { OrderId = id });
return MapToDto(response);
}
}
6. Interceptors — Middleware for gRPC
Interceptors in gRPC are similar to middleware in ASP.NET Core, allowing you to hook into the request/response pipeline. They are ideal for logging, metrics, authentication, and error handling.
graph LR
A[Client Request] --> B[Logging
Interceptor]
B --> C[Auth
Interceptor]
C --> D[Metrics
Interceptor]
D --> E[gRPC Service
Handler]
E --> D
D --> C
C --> B
B --> F[Client Response]
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#2c3e50,stroke:#fff,color:#fff
style D fill:#e94560,stroke:#fff,color:#fff
style E fill:#16213e,stroke:#fff,color:#fff
style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
=> _logger = logger;
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var sw = Stopwatch.StartNew();
var method = context.Method;
try
{
var response = await continuation(request, context);
sw.Stop();
_logger.LogInformation(
"gRPC {Method} completed in {ElapsedMs}ms",
method, sw.ElapsedMilliseconds);
return response;
}
catch (RpcException ex)
{
sw.Stop();
_logger.LogError(ex,
"gRPC {Method} failed with {StatusCode} in {ElapsedMs}ms",
method, ex.StatusCode, sw.ElapsedMilliseconds);
throw;
}
}
}
7. gRPC-Web and JSON Transcoding
A major limitation of traditional gRPC is that browsers don't support HTTP/2 trailers, making gRPC unusable directly from frontend code. .NET 10 provides two solutions:
7.1. gRPC-Web
A protocol adaptation that allows browsers to call gRPC services through HTTP/1.1 or HTTP/2 without trailers.
// Server config
builder.Services.AddGrpc();
var app = builder.Build();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.MapGrpcService<OrderApiService>().EnableGrpcWeb();
// JavaScript client (grpc-web)
const client = new OrderApiClient("https://api.example.com");
const request = new GetOrderRequest();
request.setOrderId("ORD-2026-001");
const response = await client.getOrder(request, {});
7.2. JSON Transcoding — RESTful gRPC
Expose gRPC services as RESTful JSON APIs without writing additional controllers. One gRPC service, two ways to call it.
import "google/api/annotations.proto";
service OrderApi {
rpc GetOrder (GetOrderRequest) returns (OrderResponse) {
option (google.api.http) = {
get: "/api/v1/orders/{order_id}"
};
}
rpc CreateOrder (CreateOrderRequest) returns (OrderResponse) {
option (google.api.http) = {
post: "/api/v1/orders"
body: "*"
};
}
}
// Enable JSON Transcoding
builder.Services.AddGrpc().AddJsonTranscoding();
// Now you can call either way:
// gRPC: grpcClient.GetOrderAsync(new GetOrderRequest { OrderId = "123" })
// REST: GET https://api.example.com/api/v1/orders/123
📌 When to Use What?
gRPC-Web: When you want to keep Protobuf binary format for performance, and clients are SPAs (React, Vue, Angular). JSON Transcoding: When you need to expose a RESTful API for third-party consumers or mobile apps already using REST, without maintaining two codebases.
8. gRPC vs REST vs GraphQL Comparison
| Criteria | gRPC | REST | GraphQL |
|---|---|---|---|
| Serialization | Protobuf (binary) | JSON (text) | JSON (text) |
| Transport | HTTP/2 | HTTP/1.1 or 2 | HTTP/1.1 or 2 |
| Contract | .proto (required) | OpenAPI (optional) | SDL (required) |
| Streaming | Native bidirectional | SSE / WebSocket | Subscriptions |
| Code generation | Automatic, multi-language | Via external tools | Via external tools |
| Browser support | Via gRPC-Web / proxy | Native | Native (HTTP POST) |
| Caching | No HTTP caching | Native HTTP caching | Requires extra effort |
| Payload size | Smallest (binary) | Large (verbose JSON) | Flexible (field selection) |
| Primary use case | Service-to-service | Public APIs, CRUD | BFF, data aggregation |
9. Load Balancing and Resilience
gRPC over HTTP/2 uses persistent connections, making layer 4 (TCP) load balancing ineffective since all requests travel through a single connection. You need layer 7 (application) load balancing or client-side balancing.
graph TB
subgraph ClientSide["Client-Side Load Balancing"]
C1[gRPC Client] --> DNS[DNS / Service Discovery]
DNS --> S1[Server 1]
DNS --> S2[Server 2]
DNS --> S3[Server 3]
end
subgraph ProxyBased["Proxy-Based Load Balancing"]
C2[gRPC Client] --> LB[L7 Proxy
Envoy / YARP]
LB --> S4[Server 1]
LB --> S5[Server 2]
LB --> S6[Server 3]
end
style C1 fill:#e94560,stroke:#fff,color:#fff
style C2 fill:#e94560,stroke:#fff,color:#fff
style DNS fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style LB fill:#2c3e50,stroke:#fff,color:#fff
style S1 fill:#16213e,stroke:#fff,color:#fff
style S2 fill:#16213e,stroke:#fff,color:#fff
style S3 fill:#16213e,stroke:#fff,color:#fff
style S4 fill:#16213e,stroke:#fff,color:#fff
style S5 fill:#16213e,stroke:#fff,color:#fff
style S6 fill:#16213e,stroke:#fff,color:#fff
9.1. Client-Side Load Balancing in .NET
// Configure DNS-based client-side load balancing
builder.Services.AddGrpcClient<OrderApi.OrderApiClient>(options =>
{
// Use dns:/// scheme to activate DNS resolver
options.Address = new Uri("dns:///order-service.default.svc.cluster.local");
})
.ConfigureChannel(channel =>
{
channel.ServiceConfig = new ServiceConfig
{
LoadBalancingConfigs = { new RoundRobinConfig() }
};
channel.Credentials = ChannelCredentials.Insecure; // Within cluster
});
9.2. Retry Policy
// Retry with exponential backoff
var retryPolicy = new RetryPolicy
{
MaxAttempts = 5,
InitialBackoff = TimeSpan.FromMilliseconds(100),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 1.5,
RetryableStatusCodes =
{
StatusCode.Unavailable, // Server down
StatusCode.DeadlineExceeded // Timeout
}
};
// Hedging — send multiple requests in parallel, take the first response
var hedgingPolicy = new HedgingPolicy
{
MaxAttempts = 3,
HedgingDelay = TimeSpan.FromMilliseconds(200),
NonFatalStatusCodes = { StatusCode.Unavailable }
};
⚠️ Retry vs Hedging
Retry resends the request after receiving an error — safe for any idempotent operation. Hedging sends requests before receiving an error (speculative execution) — only use for read operations or operations with idempotency keys.
10. Performance Optimization
10.1. Connection Management
// Reuse channels (1 channel = 1 HTTP/2 connection)
// Do NOT create a new channel for each request
var channel = GrpcChannel.ForAddress("https://order-service:5001",
new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
// Keep connection alive
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
// Increase concurrent streams per connection
EnableMultipleHttp2Connections = true
}
});
10.2. Compression
// Server: enable default compression
builder.Services.AddGrpc(options =>
{
options.ResponseCompressionAlgorithm = "gzip";
options.ResponseCompressionLevel = CompressionLevel.Optimal;
});
// Client: request compression
var callOptions = new CallOptions(
writeOptions: new WriteOptions(WriteFlags.NoCompress)); // Disable for small messages
10.3. Deadline Propagation
// Service A → Service B → Service C
// Deadline automatically propagates through the chain
// Service A sets the initial deadline
var deadline = DateTime.UtcNow.AddSeconds(10);
var response = await clientB.DoWorkAsync(request,
deadline: deadline);
// Service B reads remaining deadline and forwards it
public override async Task<WorkResult> DoWork(
WorkRequest request, ServerCallContext context)
{
// context.Deadline contains the deadline from caller
// When calling Service C, deadline auto-propagates
// if using EnableCallContextPropagation()
var result = await _clientC.ProcessAsync(subRequest);
return result;
}
11. Testing gRPC Services
// Integration test with WebApplicationFactory
public class OrderServiceTests : IClassFixture<GrpcTestFixture>
{
private readonly OrderApi.OrderApiClient _client;
public OrderServiceTests(GrpcTestFixture fixture)
{
_client = new OrderApi.OrderApiClient(fixture.Channel);
}
[Fact]
public async Task GetOrder_ExistingOrder_ReturnsOrder()
{
var response = await _client.GetOrderAsync(
new GetOrderRequest { OrderId = "test-order-1" });
Assert.Equal("test-order-1", response.OrderId);
Assert.Equal(OrderStatus.Confirmed, response.Status);
}
[Fact]
public async Task GetOrder_NonExistent_ThrowsNotFound()
{
var ex = await Assert.ThrowsAsync<RpcException>(
() => _client.GetOrderAsync(
new GetOrderRequest { OrderId = "non-existent" }));
Assert.Equal(StatusCode.NotFound, ex.StatusCode);
}
}
// Test fixture
public class GrpcTestFixture : IDisposable
{
private readonly WebApplicationFactory<Program> _factory;
public GrpcChannel Channel { get; }
public GrpcTestFixture()
{
_factory = new WebApplicationFactory<Program>();
var client = _factory.CreateDefaultClient(
new ResponseVersionHandler());
Channel = GrpcChannel.ForAddress(
_factory.Server.BaseAddress!,
new GrpcChannelOptions { HttpClient = client });
}
public void Dispose() => _factory.Dispose();
}
12. When to Use (and Not Use) gRPC
Conclusion
gRPC on .NET 10 is a powerful choice for inter-service communication when performance is the top priority. With Protobuf binary serialization that's 3-10x faster than JSON, HTTP/2 multiplexing, native streaming, and a mature tooling ecosystem on .NET, gRPC enables building distributed systems with low latency and high throughput. Combined with JSON Transcoding and gRPC-Web, you don't have to sacrifice compatibility with browsers or REST clients.
References:
Biome — The Rust Toolchain Replacing ESLint + Prettier, 50x Faster
Debugger Agent in Visual Studio 2026 — When AI Debugs Your Code For You
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.