gRPC Fundamentals

gRPC is a remote-procedure-call framework on top of HTTP/2, with Protobuf as the schema and serialisation language. It's fast, strongly typed, and supports four streaming modes that REST doesn't natively. It's also more operationally finicky than REST and worse at supporting browsers.

This page is when to pick it, the gotchas, and the patterns that make it work in production.

When gRPC wins

When REST wins

In practice: REST for external / public APIs; gRPC for internal service-to-service. The split is consistent across most production architectures.

The four streaming modes

service OrderService {
  // Unary: one request, one response (like REST)
  rpc GetOrder(GetOrderRequest) returns (Order);
  
  // Server streaming: one request, stream of responses
  rpc StreamOrders(GetOrdersRequest) returns (stream Order);
  
  // Client streaming: stream of requests, one response
  rpc UploadEvents(stream Event) returns (UploadResult);
  
  // Bidirectional: both ways
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

Common uses:

For services that don't need streaming, unary is enough. The streaming modes are powerful but operationally trickier (long-lived connections, partial failures).

The Protobuf contract

A proto file defines services, methods, and messages:

syntax = "proto3";
package com.example.orders;

message Order {
  string id = 1;
  string user_id = 2;
  Money total = 3;
  OrderStatus status = 4;
  google.protobuf.Timestamp created_at = 5;
}

message Money {
  string currency = 1;
  int64 amount_cents = 2;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
}

service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
}

Code generation produces server stubs and client libraries in target languages. The proto file is the source of truth; everything else is generated.

Field-numbering discipline (the load-bearing convention)

Field numbers identify fields on the wire. Once assigned, never:

message User {
  reserved 5;  // was 'phone', removed in v2.3
  reserved "phone";
  
  string id = 1;
  string email = 2;
  string name = 3;
  // 4 was previously something; reserve if you removed it
  reserved 4;
  string country = 6;
}

Field-number errors corrupt data silently. The reserve discipline is non-negotiable.

Common patterns

Pagination

message ListOrdersRequest {
  string user_id = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListOrdersResponse {
  repeated Order orders = 1;
  string next_page_token = 2;
}

Cursor-based pagination using opaque tokens. Standard pattern.

Error handling

gRPC has its own status codes (different from HTTP). Common ones:

Map these correctly; clients depend on the status to decide retry vs surface.

For richer error info, attach google.rpc.Status with details (a message detailing what went wrong, which field, what to do).

Authentication

Two layers:

Both are common. mTLS is the default for service mesh; bearer tokens for user-context calls.

Retries

gRPC's retry policy is configured per service:

{
  "retryPolicy": {
    "maxAttempts": 4,
    "initialBackoff": "1s",
    "maxBackoff": "10s",
    "backoffMultiplier": 2,
    "retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
  }
}

Idempotency: only retry idempotent operations. POST-equivalent calls must be idempotent if you retry; otherwise duplicate side effects.

Deadlines

gRPC supports per-call deadlines: "this call must complete within X seconds." Propagate the remaining deadline through dependent calls so a slow downstream doesn't blow your overall budget.

This is the equivalent of context.Context in Go; standard discipline.

The browser problem

Browsers can't speak gRPC natively because they don't expose HTTP/2 trailers (which gRPC uses for status). Workarounds:

For mobile, gRPC clients work natively (iOS, Android). For browsers in 2026, Connect is the smoothest path.

Operational concerns

Schema evolution

Use a registry (Buf, Apicurio) and compatibility checks. See SchemaRegistryAndEvolution. Without it, breaking changes ship silently.

Observability

gRPC interceptors are the equivalent of HTTP middleware. Use them for:

Without these, gRPC services are harder to debug than REST.

Load balancing

gRPC over HTTP/2 multiplexes many calls on one connection. This breaks naive layer-4 load balancing — all calls from one client go to one server.

Solutions:

Misconfigured load balancing produces "all traffic goes to one pod" surprises. Usually surfaces under load.

TLS termination

Service mesh (Linkerd, Istio) typically handles mTLS at the sidecar level. Without a mesh, configure TLS in the gRPC server explicitly.

Don't expose gRPC over plain HTTP/2 in production. Always TLS.

Tooling

Migration patterns

If you're moving from REST to gRPC for internal service-to-service:

  1. Define proto files for existing endpoints.
  2. Generate gRPC servers alongside existing REST.
  3. Migrate clients one at a time.
  4. Retire REST once all clients have moved.

Run both during transition. The proto file becomes the source of truth; REST becomes a generated facade for legacy consumers via grpc-gateway.

When to skip gRPC

Further reading