gRPC in .NET 10 — High-Performance Microservices Communication with Protobuf

Posted on: 4/26/2026 9:11:37 AM

3-10x Faster than JSON (Protobuf serialization)
HTTP/2 Multiplexing — no head-of-line blocking
40+ Languages supported via code generation
4 Patterns Streaming: Unary, Server, Client, Bidirectional

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
gRPC architecture overview: Contract-first with Protobuf + HTTP/2

1.1. Core Advantages of gRPC

  • Contract-first development: Define APIs in .proto files, 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
doubledouble64-bit floating point
floatfloat32-bit floating point
int32intVarint encoding, inefficient for negatives
int64longVarint encoding
boolbool
stringstringUTF-8 encoded
bytesByteStringArbitrary binary data
google.protobuf.TimestampDateTimeOffsetUse 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
  
4 gRPC communication patterns — from simple to complex

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

.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
gRPC interceptor pipeline — similar to a middleware chain
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
Two load balancing strategies for gRPC

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

Use: Service-to-service communication in microservices
Use: Realtime streaming (notifications, live data)
Use: Polyglot systems (multi-language communication)
Avoid: Public APIs for third-party consumers (use REST)
Step 1 — Evaluate
Determine if service communication is actually a bottleneck. If REST + JSON is performing well, no need to migrate.
Step 2 — Proto-first
Design APIs in .proto files before writing code. This is an opportunity to standardize contracts across teams.
Step 3 — Hybrid
Deploy gRPC for internal services, keep REST/GraphQL for external APIs. JSON Transcoding helps serve both from the same codebase.
Step 4 — Observability
Add interceptors for logging, tracing (OpenTelemetry), and metrics from the start. gRPC reflection enables easy debugging.

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: