Andrew Mercer
on this page

What gRPC is

gRPC ("gRPC Remote Procedure Calls") is an open-source RPC framework originally developed by Google, built on top of HTTP/2 and Protocol Buffers (protobuf). Instead of hand-rolling a REST API and a matching client, you define a service's methods and message types once in a .proto file, and gRPC's code generator produces client and server code in whatever languages you need — Go, Python, Java, C++, Rust, Node.js, and more all interoperate against the same .proto contract.

It's commonly used for:

  • Service-to-service communication inside a microservices architecture
  • Low-latency, high-throughput internal APIs
  • Streaming data between services (telemetry, logs, real-time updates)
  • Polyglot systems where different services are written in different languages but need a strongly-typed contract between them

Why gRPC over REST/JSON

REST/JSON gRPC
Transport HTTP/1.1 (usually) HTTP/2
Payload format JSON (text, larger) Protobuf (binary, compact)
Contract Often informal (OpenAPI optional) Enforced .proto schema
Streaming Awkward (polling, SSE, WebSockets bolted on) Native (all four RPC types)
Codegen Third-party (OpenAPI generators) Built-in, first-class
Browser support Native Needs gRPC-Web + proxy
Human readability High (plain text) Low (binary on the wire)

gRPC trades human-readability and universal browser support for speed, strict contracts, and native streaming. REST/JSON is usually still the better choice for public-facing APIs consumed by browsers or third parties unfamiliar with your schema; gRPC shines for internal service-to-service traffic where both ends are under your control.

Protocol Buffers (protobuf)

Protobuf is gRPC's default Interface Definition Language (IDL) and serialization format. A .proto file defines your messages (data structures) and services (RPC method signatures).

syntax = "proto3";

package user.v1;

message GetUserRequest {
  string user_id = 1;
}

message User {
  string user_id = 1;
  string name = 2;
  string email = 3;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
}

Key points:

  • The numbers after each field (= 1, = 2) are field tags, not default values — they identify the field on the wire and must never be reused or changed once a message has shipped, or you'll silently corrupt data for anyone still on an older schema version.
  • proto3 is the current syntax version in near-universal use (proto2 still exists in legacy codebases).
  • Protobuf is strongly typed — no free-form JSON blobs — which is what allows gRPC to generate real client/server code rather than just documentation.
  • Compiled with protoc (the Protocol Buffer compiler) or, increasingly, buf, which wraps protoc with better dependency management, linting, and breaking-change detection.

Compiling

protoc --go_out=. --go-grpc_out=. user.proto

This generates language-specific message structs/classes and client/server stub code from the .proto file. Each language needs its own protoc plugin (protoc-gen-go, protoc-gen-python, etc.).

The four RPC types

gRPC supports four communication patterns, all defined directly in the .proto file via the stream keyword:

1. Unary RPC

The standard request/response — one request in, one response out. Functionally equivalent to a typical REST call.

rpc GetUser(GetUserRequest) returns (User);

2. Server streaming RPC

Client sends one request; server responds with a stream of messages over time. Useful for things like a live feed, paginated results streamed incrementally, or a long-running job's progress updates.

rpc ListUserEvents(ListUserEventsRequest) returns (stream UserEvent);

3. Client streaming RPC

Client sends a stream of messages; server responds once, after the client finishes sending. Useful for uploading data in chunks (e.g., a large file, batched sensor readings) before getting a single confirmation back.

rpc UploadEvents(stream UserEvent) returns (UploadSummary);

4. Bidirectional streaming RPC

Both sides stream messages independently and concurrently over a single connection. Useful for chat-like protocols, real-time collaborative editing, or any case where both sides need to push data to each other without waiting.

rpc Chat(stream ChatMessage) returns (stream ChatMessage);

Channels and stubs

A channel is the client-side abstraction representing a connection to a gRPC server (host + port), handling connection management, retries, and load balancing under the hood. A stub is generated client code built on top of a channel that exposes the service's RPC methods as regular-looking function calls.

channel = grpc.insecure_channel("localhost:50051")
stub = user_pb2_grpc.UserServiceStub(channel)
response = stub.GetUser(user_pb2.GetUserRequest(user_id="123"))

Channels are meant to be long-lived and reused across many calls — creating a new channel per request defeats connection reuse and adds unnecessary latency.

Deadlines and timeouts

gRPC encourages setting a deadline on every call (an absolute point in time by which the call must complete) rather than a simple timeout duration, since deadlines propagate correctly across a chain of downstream calls.

stub.GetUser(request, timeout=5)  # seconds, most language bindings accept this shorthand

If service A calls service B which calls service C, and A sets a 2-second deadline, that deadline should propagate through B into C — so C doesn't keep working on a request A has already given up waiting for. Failing to propagate deadlines is a common source of wasted work and cascading latency in gRPC-based microservices.

Error handling: status codes

gRPC uses a fixed, transport-level set of status codes (distinct from HTTP status codes) that every language binding maps to consistently:

Code Meaning
OK Success
CANCELLED Caller cancelled the call
INVALID_ARGUMENT Client sent invalid data
DEADLINE_EXCEEDED Call didn't finish before the deadline
NOT_FOUND Requested entity doesn't exist
ALREADY_EXISTS Entity already exists (e.g. create conflict)
PERMISSION_DENIED Caller lacks permission
UNAUTHENTICATED Caller isn't authenticated
RESOURCE_EXHAUSTED Rate limit or quota hit
FAILED_PRECONDITION System not in a state to perform the operation
UNAVAILABLE Service is down or unreachable (safe to retry)
INTERNAL Server-side bug/invariant violation

Each status can carry a human-readable message and, optionally, structured error details via the google.rpc.Status and google.rpc.ErrorDetails protobuf types — giving you machine-parseable error metadata beyond a plain string, which plain HTTP status codes don't offer natively.

Interceptors (middleware)

Interceptors are gRPC's equivalent of HTTP middleware — code that wraps every call to handle cross-cutting concerns without touching individual RPC method implementations:

  • Authentication/authorization checks
  • Logging and request tracing
  • Metrics collection (latency, error rate per method)
  • Automatic retries with backoff
  • Rate limiting

Both client-side and server-side interceptors exist, and they can be chained.

Authentication and transport security

  • TLS is the standard way to encrypt gRPC traffic in transit; production gRPC services should essentially never run with insecure_channel/plaintext credentials outside of local development.
  • Token-based auth (e.g. OAuth2 bearer tokens, API keys) is typically attached via call credentials, sent as gRPC metadata (gRPC's equivalent of HTTP headers) on each call, layered on top of the transport-level TLS channel credentials.
  • mTLS (mutual TLS, where both client and server present certificates) is common in service-mesh environments (e.g. Istio, Linkerd) to authenticate service identity without embedding application-level tokens at all.

Load balancing

Because gRPC connections are long-lived HTTP/2 connections (not one-connection-per-request like typical REST/JSON over HTTP/1.1), naive L4 (TCP) load balancing tends to pin a client to one backend for the life of the connection, which can cause uneven load distribution. Common approaches:

  • Client-side load balancing: the gRPC client resolves multiple backend addresses (e.g. via DNS or a service registry) and distributes RPCs across them itself.
  • Proxy-based (L7) load balancing: a proxy that understands HTTP/2 framing (e.g. Envoy, Linkerd, or a cloud load balancer with gRPC support) load-balances at the individual-request level rather than the connection level.
  • Service mesh: in a Kubernetes environment, a mesh (Istio, Linkerd) commonly handles gRPC-aware load balancing, retries, and mTLS transparently via sidecar proxies.

gRPC-Web

Browsers can't originate raw HTTP/2 gRPC calls directly (no browser API exposes the necessary low-level framing control), so gRPC-Web exists as a JavaScript client library plus a small wire-protocol variant that a proxy (typically Envoy) translates into standard gRPC calls to your backend. gRPC-Web supports unary and server-streaming calls; client-streaming and bidirectional streaming aren't supported from the browser.

Reflection and CLI tooling

  • Server reflection: an optional gRPC service a server can expose that lets clients discover its available services/methods at runtime without needing the .proto file in advance — useful for debugging tools and API explorers.
  • grpcurl: the curl of gRPC — a CLI tool for making ad hoc gRPC calls from the command line, especially useful combined with server reflection so you don't need to supply a .proto file.
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"user_id": "123"}' localhost:50051 user.v1.UserService/GetUser
  • Evans / BloomRPC / Postman: GUI or REPL-style tools for exploring and testing gRPC services interactively, analogous to Postman for REST.

Health checking

gRPC defines a standard health checking protocol (grpc.health.v1.Health) that services can implement so load balancers, orchestrators (like Kubernetes), and service meshes have a consistent way to ask "are you healthy?" without each service inventing its own bespoke health-check RPC.

Versioning and backward compatibility

Protobuf's field-tag-based binary format is specifically designed to make backward/forward compatibility straightforward, provided you follow a few rules:

  • Never change or reuse a field's tag number.
  • Never change a field's type after it's shipped (add a new field instead).
  • New fields must be optional (proto3 fields are optional-by-default) so old clients/servers that don't know about them simply ignore them.
  • Removing a field: mark its tag reserved rather than letting a future field accidentally reuse that number.
  • For breaking changes, version the package/service (user.v1 → user.v2) rather than mutating the existing contract in place.

Tools like buf breaking can automatically check a .proto change against its previous version and fail CI if it introduces a wire-incompatible change.

When not to reach for gRPC

  • Public APIs consumed directly by third-party developers who expect plain JSON/REST and OpenAPI docs.
  • Browser-facing APIs where you don't want to run a gRPC-Web proxy just to support fetch-based clients.
  • Simple CRUD services where REST's ubiquity and tooling (curl, Postman, browser devtools) outweigh gRPC's performance benefits.
  • Situations where human-readability of the wire payload matters for debugging (protobuf's binary format needs the .proto schema to decode, unlike JSON).

Quick reference

Concept One-line summary
.proto file Defines messages and services; the contract between client and server
protoc / buf Compiles .proto into language-specific code
Channel Client-side connection abstraction, reused across calls
Stub Generated client exposing RPC methods as function calls
Deadline Absolute time by which a call must complete; propagates across service calls
Status code gRPC's fixed error-code set (NOT_FOUND, UNAVAILABLE, etc.)
Interceptor Middleware for cross-cutting concerns (auth, logging, metrics)
gRPC-Web Browser-compatible variant requiring a translating proxy
Health checking Standard grpc.health.v1.Health service for liveness/readiness

Further reading

  • https://grpc.io/docs/
  • https://protobuf.dev/
  • https://buf.build/docs/