gRPC is a contract-first RPC framework. A service definition declares methods and message types. Tooling generates clients and server bases for the selected languages. Protocol Buffers is the default interface definition language and message encoding, while HTTP 2 supplies multiplexed streams and flow control.

The framework fits services whose clients and servers can share a versioned contract. Its harder production problems sit outside serialization: deadlines, ambiguous failures, connection-aware load balancing, schema evolution, and telemetry.

How It Works

gRPC over HTTP/2

Each gRPC call occupies one HTTP/2 stream, represented by frames inside a connection. Independent streams can make progress without waiting for an earlier HTTP response to finish. They still share the underlying TCP connection, so packet loss can stall every stream on that connection.

sequenceDiagram
  participant Client
  participant Server

  Client->>Server: HEADERS frame with POST method path grpc-timeout metadata
  Client->>Server: DATA frames length-prefixed protobuf message
  Server->>Client: HEADERS frame initial metadata
  Server->>Client: DATA frames length-prefixed protobuf response
  Server->>Client: HEADERS frame with END_STREAM grpc-status grpc-message

The final gRPC status normally travels in HTTP/2 trailers rather than being expressed only by the HTTP status code. A transport-level load balancer cannot inspect that application status or distribute individual streams because it sees connections. GRPC-Web uses its own framing so browser clients can receive equivalent status information despite browser API limits around trailers.

Flow Control and Backpressure

HTTP/2 flow control maintains connection and stream windows. When available credit is exhausted, further writes wait until the receiver consumes data and sends WINDOW_UPDATE. This creates transport backpressure, although application code still needs bounded buffering before it reaches the stream writer.

Kestrel’s default initial stream window is 768 KiB. Larger windows can reduce stop-start transfer for large messages, but they also allow more memory to be buffered per active stream. InitialConnectionWindowSize must be at least as large as InitialStreamWindowSize.

Streaming Patterns

PatternClient sendsServer sendsTypical use
UnaryOne messageOne messageCommands and point lookups
Server streamingOne messageA message streamIncremental query results or event feeds
Client streamingA message streamOne messageIncremental upload or aggregation
BidirectionalA message streamA message streamLong-lived conversations with independent flow in each direction

Server Streaming Example

service OrderService {
  rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);
}
// Server
public override async Task ListOrders(
    ListOrdersRequest request,
    IServerStreamWriter<OrderResponse> responseStream,
    ServerCallContext context)
{
    await foreach (var order in _repository.GetOrdersAsync(
        request.CustomerId, context.CancellationToken))
    {
        await responseStream.WriteAsync(order);
    }
}
 
// Client
using var call = client.ListOrders(new ListOrdersRequest { CustomerId = "cust-42" });
await foreach (var order in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine($"Order {order.Id}: {order.Total}");
}

Only one write may be pending on a request or response stream. Multiple producers therefore need one serialization point, such as a bounded Channel<T> drained by a single writer task.

.NET Integration

Channel Management

A GrpcChannel owns the HTTP transport used by generated clients. Channels and clients are safe for concurrent use, and reusing them avoids repeating connection and TLS setup for every call.

var handler = new SocketsHttpHandler
{
    PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
    KeepAlivePingDelay = TimeSpan.FromSeconds(60),
    KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
    EnableMultipleHttp2Connections = true
};
 
var channel = GrpcChannel.ForAddress("https://order-service:5001", new GrpcChannelOptions
{
    HttpHandler = handler
});
var client = new OrderService.OrderServiceClient(channel);

HTTP/2 peers negotiate a maximum number of concurrent streams. Many servers use 100 by default. EnableMultipleHttp2Connections = true lets the handler open another connection after that negotiated limit is reached instead of leaving new calls queued behind long-lived streams. GrpcChannel configures this behavior for its default handler, while a custom handler must set it explicitly.

Keep-alive pings can keep an otherwise idle connection ready, but only when the server and intermediaries accept the policy. A server that rejects excessive pings may send GOAWAY and close the connection.

Deadline Propagation

gRPC does not set a deadline by default. A stalled call can therefore keep request state and downstream work alive until another timeout or connection failure ends it. Each operation needs a deliberate deadline derived from its latency budget.

// Manual: set deadline on outgoing call
var reply = await client.GetOrderAsync(
    request,
    deadline: DateTime.UtcNow.AddSeconds(5));
 
// Automatic: propagate incoming deadline to downstream calls
services.AddGrpcClient<OrderServiceClient>(o =>
        o.Address = new Uri("https://order-service:5001"))
    .EnableCallContextPropagation();

EnableCallContextPropagation forwards the current server call’s deadline and cancellation into an outgoing call. A shorter explicit child deadline still wins. Across process boundaries, gRPC propagates the remaining timeout rather than an absolute timestamp, avoiding dependence on synchronized clocks.

Interceptors

Interceptors wrap gRPC calls at the generated-client or service-method boundary. Server interceptors receive typed messages and call context, while ASP.NET Core middleware runs at the HTTP pipeline boundary.

  • Use middleware for connection-wide HTTP concerns and authentication infrastructure shared with other ASP.NET Core endpoints.
  • Use interceptors for per-RPC metrics, metadata policy, status mapping, or cross-cutting logic that needs the gRPC method and call context.

Interceptor order is observable. Keep a short chain and test the registration path used by the client factory or channel rather than relying on visual order alone.

Error Model and Retries

gRPC defines application status codes separate from HTTP status: OK, NOT_FOUND, INVALID_ARGUMENT, DEADLINE_EXCEEDED, UNAVAILABLE, and others. A .NET server can throw RpcException. The client receives an RpcException and inspects StatusCode. Structured error details may use the google.rpc.Status model, subject to library support and metadata size limits.

Built-in retries are configured declaratively through the service config:

var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
    ServiceConfig = new ServiceConfig
    {
        MethodConfigs = { new MethodConfig
        {
            Names = { MethodName.Default },
            RetryPolicy = new RetryPolicy
            {
                MaxAttempts = 4,
                InitialBackoff = TimeSpan.FromSeconds(1),
                BackoffMultiplier = 2,
                RetryableStatusCodes = { StatusCode.Unavailable }  // only safe codes
            }
        }}
    }
});

A retryable status code describes the failure, not whether repeating the operation is safe. Retry only calls whose contract is idempotent or deduplicated, and cap attempts within the deadline. An ambiguous failure on a write can otherwise apply the change twice, the RPC delivery-semantics problem. Hedging sends overlapping attempts and therefore belongs only on operations that tolerate concurrent duplication.

Pitfalls

L4 Load Balancing Sees Connections, Not Calls

An L4 load balancer chooses a backend for each TCP connection. All RPCs multiplexed on that connection follow the same route, so a small number of long-lived channels can produce uneven backend load.

An HTTP/2-aware L7 proxy can balance calls before a stream is established. Client-side load balancing is the other common design: the channel resolves endpoints and selects one for each new call. Once a streaming call begins, its messages remain on the selected backend.

Missing Deadlines Leave Work Without a Budget

Suppose service A gives an operation two seconds. Service B spends part of that budget, then calls C without propagating the deadline. A may abandon the result while C keeps running with no knowledge that the original budget expired.

Client-factory context propagation carries the remaining deadline into downstream gRPC calls. Application code must also pass ServerCallContext.CancellationToken to database, HTTP, and other cancellable operations. Cancellation is cooperative. A token cannot stop code that ignores it.

Proto Field Numbers Are Wire Identities

Renumbering a field deletes one wire identity and creates another. Reusing the old number for a different meaning can cause parse failures, data loss, or values being interpreted under the wrong schema. Field names are not present in the binary key.

Existing field numbers must not change. After deleting a field, reserve its number and, where JSON or text-format compatibility matters, its name:

message UserRequest {
  reserved 5;
  reserved "old_field_name";
  string user_id = 1;
}

Browser Transports Support Fewer Streaming Shapes

Mainstream gRPC-Web clients support unary calls and, with the compatible transport mode, server streaming. They do not expose native client streaming or bidirectional streaming through current browser APIs.

Browser requirements belong in the contract decision. A service can expose supported gRPC-Web methods, add ASP.NET Core JSON transcoding for mapped HTTP/JSON endpoints, or define a separate WebSocket protocol when full duplex browser messaging is the real requirement.

Tradeoffs

CriteriongRPCREST/JSON
ContractService definition and message schema requiredOpenAPI is optional
PayloadProtobuf is compact for many schemasJSON is text and easy to inspect
StreamingAll 4 patterns nativelyWorkarounds needed via SSE or WebSocket
Browser supportRequires gRPC-Web or JSON transcodingNative
Human-readable wire formatNoYes
Generic HTTP toolingRequires gRPC-aware reflection or schema supportBroad curl, proxy, and browser support
HTTP cachingUnary calls use POST and do not gain normal HTTP response caching by defaultCache semantics are available when the API uses them correctly

gRPC is a strong fit for controlled service estates that benefit from generated contracts or streaming. REST-style HTTP APIs remain easier to expose to arbitrary clients and intermediaries. A gateway can serve both boundaries, but it also creates two observable protocols whose errors, timeouts, and compatibility rules need deliberate mapping.

References