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.
curl against a gRPC endpoint is awkward; against REST, trivial.In practice: REST for external / public APIs; gRPC for internal service-to-service. The split is consistent across most production architectures.
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).
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 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.
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.
gRPC has its own status codes (different from HTTP). Common ones:
OK — success.INVALID_ARGUMENT — client error in input.NOT_FOUND — resource doesn't exist.PERMISSION_DENIED — auth issue.UNAUTHENTICATED — no/bad credentials.RESOURCE_EXHAUSTED — quota exceeded.FAILED_PRECONDITION — system in wrong state for this operation.INTERNAL — server error.UNAVAILABLE — service temporarily down.DEADLINE_EXCEEDED — timeout.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).
Two layers:
authorization header.Both are common. mTLS is the default for service mesh; bearer tokens for user-context calls.
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.
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.
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.
Use a registry (Buf, Apicurio) and compatibility checks. See SchemaRegistryAndEvolution. Without it, breaking changes ship silently.
gRPC interceptors are the equivalent of HTTP middleware. Use them for:
Without these, gRPC services are harder to debug than REST.
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.
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.
grpcurl — command-line gRPC client; like curl for gRPC.grpcui — web UI for gRPC services.If you're moving from REST to gRPC for internal service-to-service:
Run both during transition. The proto file becomes the source of truth; REST becomes a generated facade for legacy consumers via grpc-gateway.