Skip to content

gRPC

Remote Procedure Call (RPC) over HTTP/2 with Protocol Buffers.

What is gRPC

An open-source, high-performance RPC framework. A client calls a method on a server on a different machine as if it were a local object — gRPC handles the networking. Runs on HTTP/2, uses Protocol Buffers as its Interface Definition Language (IDL) and wire format by default.

The whole architecture at a glance — the layered stack (Application → Stub → gRPC Runtime → Transport) plus the pluggable pieces this note drills into below (interceptors, name resolution / service discovery, load balancing, retry / deadline / cancellation):

gRPC architecture overview: the Application → Stub → gRPC Runtime → Transport layers, with channel, Protobuf plugin, and API core, and callouts for interceptor, name resolver, load balancer, retry/deadline/cancellation, and termination

Source: Ivy Zhuang’s talk — gRPC overview (gRPCConf 2024).

Start here: the Service Definition

Everything flows from the contract. gRPC is built around the idea of defining a service — specifying the methods that can be called remotely with their parameters and return types.

  1. Write a .proto file — the contract (message = data shapes, service = the RPCs).
  2. Run protoc (with the gRPC plugin) → generates client stubs and server interfaces in your language.
  3. Server implements the interface; client calls the stub.
  4. Because both sides are generated from the same .proto, they can’t drift out of sync.

Core concepts — Service definition

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest  { string greeting = 1; }

message HelloResponse { string reply = 1; }
My hands-on version of this whole loop (Python + SQLite) lives in the repo at projects/slack-clone/message-service.

Protobuf’s dual role

Protocol Buffers is both:

  • the IDL (Interface Definition Language) — the language you define messages and services in, and
  • the serialization mechanism — a compact, strongly-typed binary format on the wire (smaller/faster than JSON text).

Use proto3 with gRPC. → https://protobuf.dev

Why the binary encoding is small

JSON ships the field names as text on every message. Protobuf ships a tag number instead, plus varint length encoding.

{"id":"123","name":"john doe"}          // JSON, ~30 bytes, names on the wire
0A 03 31 32 33 12 08 6A 6F 68 6E ...     // protobuf, ~15 bytes: <tag><len><bytes>

Each field on the wire is (field_number << 3) | wire_type. That’s why field numbers are the permanent identity of a field and renaming is free but renumbering is a breaking change.

Four kinds of service methods

TypeShapeProto signature
Unary1 req → 1 resprpc SayHello(HelloRequest) returns (HelloResponse);
Server streaming1 req → stream of resprpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
Client streamingstream of req → 1 resprpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
Bidirectional streamingstream ↔ stream (independent read/write)rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

The stream keyword on the request/response side is what distinguishes them. Bidi streams are independent — order is preserved within each direction, but the two directions aren’t lock-stepped.

Core concepts — the four service methods

RPC lifecycle

Core concepts — RPC life cycle. Key topics:

  • Deadlines / timeouts — client sets how long it’ll wait; server can check if it’s still worth continuing. This is a deadline (absolute point in time), propagated across hops — better than a per-hop timeout. Deadlines blog
  • Cancellation — either side can cancel; ends the RPC, no more work done.
  • RPC termination — client and server decide independently that the call is complete.
  • Metadata — key/value info about the call, separate from the payload (auth tokens, tracing). guide
  • Channels — a client’s virtual connection to a server (host/port) with configurable state (backed by ≥1 HTTP/2 connection).

Error model

gRPC has its own status codes, not HTTP status codes: OK, INVALID_ARGUMENT, NOT_FOUND, DEADLINE_EXCEEDED, UNAVAILABLE, RESOURCE_EXHAUSTED, etc. UNAVAILABLE is the retryable one.

Interceptors (the middleware story)

Client- and server-side hooks for auth, logging, metrics, retries — the gRPC equivalent of HTTP middleware. Interceptors guide

Retries & hedging

gRPC can retry failed calls and hedge (race duplicate calls to cut tail latency) declaratively via service config — no app code. Hedging is a general distributed-systems technique, not a gRPC feature; see Request Hedging for the concept and tradeoffs. gRPC retry/hedging guide

Why HTTP/2 matters

gRPC rides on HTTP/2, which gives it:

  • Multiplexing — many concurrent calls (streams) over one TCP connection; no app-layer head-of-line blocking between calls.
  • Native bidirectional streaming — not just request/response.
  • Binary framing — efficient, compact (pairs well with binary protobuf).
  • Header compression (HPACK — Header Compression for HTTP/2) — less per-call overhead.
  • Persistent connections — avoids repeated handshakes.

HTTP/2 — RFC 9113 · deeper: High Performance Browser Networking, HTTP/2 chapter (Ilya Grigorik, free online)

HTTP/2 multiplexing still runs over a single TCP stream, so a lost packet stalls all multiplexed streams (TCP-level head-of-line blocking). HTTP/3 / QUIC fixes this by moving to UDP with per-stream loss recovery.

Where to use it

  • Internal, service-to-service comms in microservices — strong typing catches errors at compile time; binary is far more efficient than JSON/HTTP (benchmarks cite up to ~10× throughput). Best when latency is network-dominated.
  • Not for public-facing APIs / clients you don’t control — tooling is less ubiquitous than JSON-over-HTTP, and browsers can’t speak raw gRPC.
  • Common pattern: gRPC internal, REST external.

Browser gap → gRPC-Web

Browsers can’t do raw gRPC (no access to HTTP/2 frames / trailers). gRPC-Web is a JS client + a proxy (Envoy or the standalone proxy) that translates.

Load balancing gRPC (the sharp edge at scale)

A naive Layer 4 (L4, the transport layer — TCP/UDP) load balancer (LB) breaks gRPC balancing: because gRPC keeps one long-lived HTTP/2 connection and multiplexes all calls over it, connection-level (L4) balancing pins a client to one backend and every request rides that same pin. You need request-level balancing at Layer 7 (L7, the application layer — HTTP), or client-side balancing.

  • gRPC has built-in client-side load balancing (pick_first, round_robin) + xDS (the discovery protocol family behind Envoy — Listener/Route/Cluster/Endpoint Discovery Service, *DS) for dynamic discovery/config from a control plane (the same API Envoy uses).
  • gRPC load balancing blog · Custom LB policies · gRPC + xDS

Talks & deeper resources

(cited by title/speaker/venue; verify the exact URL before relying on it)

  • Ivy ZhuanggRPC overviewYouTube. SWE at Google & gRPC Java maintainer.
  • Official gRPC guides index — auth, retries, keepalive, health checking, reflection, deadlines.

Quick self-check (recall from memory)

  1. Why can’t a browser call a gRPC service directly, and what’s the fix?
  2. Why does a plain L4 load balancer distribute gRPC connections fine but requests badly?
  3. What’s safe vs breaking when editing a .proto? (rename field? renumber? delete?)
  4. Deadline vs timeout — why does gRPC prefer deadlines across a call chain?
  5. When would you pick server-streaming over just returning a repeated field?