15

gRPC

Contract-first RPC over HTTP/2: Protocol Buffers, streaming, deadlines, and when it beats REST.

What RPC means, and how it differs from REST

RPC stands for remote procedure call: you call a function that happens to run on another machine, and the framework hides the network so the call looks almost like an ordinary local function call. You say getUser(42) and get a User back; the machinery underneath serializes the arguments, ships them across the wire, runs the function on a server, and ships the result home.

REST takes a different mental model. In REST you manipulate resources named by URLs (GET /users/42) using a small fixed set of HTTP verbs (GET, POST, PUT, DELETE). The unit of thought is a noun you act on. In RPC the unit of thought is a verb you invoke — a named method with typed inputs and outputs. Neither is wrong; they optimize for different things.

AspectRESTgRPC (RPC)
Mental modelResources + verbs (nouns)Methods you call (verbs)
Payload formatUsually JSON (text)Protocol Buffers (binary)
ContractOften informal / OpenAPIA .proto file, mandatory
TransportHTTP/1.1 or HTTP/2HTTP/2 only
StreamingAwkward (SSE, polling)First-class, both directions
Human-readable on the wireYesNo (binary)
Browser-nativeYesNo (needs grpc-web)

gRPC is Google's open-source RPC framework. Its two defining choices are that every service is described up front in a schema file (so client and server code are generated, never hand-written), and that it runs over HTTP/2 with a compact binary payload. The rest of this day unpacks what those two choices buy you.

Protocol Buffers: the schema and the contract

Protocol Buffers (protobuf) is the interface definition language (IDL) and binary serialization format at the heart of gRPC. You write a .proto file that declares your message shapes and your service methods once; a code generator (protoc) then emits typed client and server classes in whatever languages you need. The .proto file is the contract — there is no separate document that can drift out of sync.

syntax = "proto3";
package user.v1;

message GetUserRequest {
  int64 user_id = 1;
}

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  repeated string roles = 4;
}

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

Two details carry most of the weight. First, every field has a field number (= 1, = 2, ...). That number, not the field name, is what gets written on the wire — the name exists only for humans and generated code. Second, repeated means "a list of," and the primitive types (int64, string, bool, and so on) map to native types in each target language. From this one file, protoc generates a UserServiceClient you can call and a UserServiceBase you implement on the server, in Go, Java, Python, C++, and more — all guaranteed to agree because they came from the same source.

Why the wire format is small and fast

The lead-in idea: JSON ships the field names as text on every message, while protobuf ships only compact field numbers and packed binary values. That difference is why protobuf payloads are typically a fraction of the size and much cheaper to parse.

Consider the User above with id 42, name "Sara", email "s@x.io". As JSON it is roughly 60 bytes of text, and the parser must scan strings, match quotes, and re-parse "42" into a number. As protobuf it is a handful of bytes: for each field, a tiny tag (the field number plus a 3-bit wire-type code) followed by the value encoded compactly — integers use a variable-length encoding where small numbers take one byte.

PropertyJSONProtobuf
Field identity on wireFull field name, every messageField number (1–2 bytes)
NumbersText digits, re-parsedVarint / fixed binary
Typical sizeBaseline~30–60% smaller
Parse costString scanningDirect binary decode
Self-describingYes (readable alone)No (needs the .proto)

The trade is explicit: you lose human readability and the ability to understand a message without its schema, and you gain size and speed. For internal service-to-service traffic where volume is high and no human is reading the bytes, that trade is usually worth it.

Backward and forward compatibility via field numbers

A running system evolves: you add fields, deprecate others, and deploy new and old versions side by side. Protobuf is built so that old and new code can exchange messages safely, and the mechanism is entirely about those field numbers — never the names.

The rules that keep evolution safe:

  • Adding a field is always safe. Give it a new, never-before-used number. Old readers that do not know the field simply ignore it (they skip any tag they do not recognize). New readers see it when present.
  • Never reuse or renumber a field. If field 4 once meant roles, that number is burned forever. Reusing it makes a new-code writer's bytes be misread by anyone still expecting the old meaning.
  • Renaming a field is harmless on the wire. The name is not transmitted, so renaming email to email_address (keeping number 3) changes nothing over the network — only your source code.
  • Removing a field: stop using it and reserve its number with reserved 4; so no future edit accidentally recycles it.
message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  repeated string roles = 4;
  string avatar_url = 5;   // added later — old clients ignore it
  reserved 6;              // a removed field's number, fenced off forever
}

This is what "backward compatible" (new server, old client still works) and "forward compatible" (old server, new client still works) mean concretely here: because unknown fields are skipped and known numbers never change meaning, both directions hold as long as you only add with fresh numbers and never reuse an old one.

HTTP/2: one connection, many calls at once

gRPC runs exclusively on HTTP/2, and that choice is what makes streaming and high concurrency practical. The key HTTP/2 feature is multiplexing: many independent requests and responses share a single TCP connection at the same time, each carried on its own logical stream inside that connection.

Under HTTP/1.1, a connection handles one request at a time; a slow response blocks everything behind it (head-of-line blocking), so clients open many connections to get concurrency. HTTP/2 lets dozens of calls interleave on one connection, each making progress independently. It also compresses headers and, crucially, allows a stream to stay open and carry a sequence of messages in either direction — which is exactly what the streaming call types below rely on.

The four call types

Because HTTP/2 streams can carry many messages over time, gRPC offers four shapes of call, defined by whether the client, the server, or both send a stream of messages instead of a single one. You choose the shape per method in the .proto.

  • Unary — one request, one response. The ordinary function call; use it for the vast majority of methods (GetUser, CreateOrder).
  • Server-streaming — one request, a stream of responses. The server keeps sending until it is done. Good for "give me all results as they are ready," a live feed, or downloading a large result in chunks.
  • Client-streaming — a stream of requests, one response. The client sends many messages, then the server replies once. Good for uploads or bulk ingestion where the server aggregates and answers at the end.
  • Bidirectional streaming — both sides stream independently on the same call. Neither has to wait for the other. Good for chat, live collaboration, or any long-lived interactive session.

In the .proto, the stream keyword marks which side streams:

service ChatService {
  rpc Send(Message) returns (Ack);                          // unary
  rpc Subscribe(Room) returns (stream Message);             // server-streaming
  rpc Upload(stream Chunk) returns (UploadResult);          // client-streaming
  rpc Chat(stream Message) returns (stream Message);        // bidirectional
}

Deadlines, metadata, and status codes

Three cross-cutting features come with every gRPC call regardless of its shape, and understanding them separates a toy client from a production one.

A deadline is the client's declaration of how long it is willing to wait — expressed as an absolute point in time — and it propagates. If service A calls B with a 2-second deadline and B calls C, C learns how much time is left and can give up early instead of doing work whose result nobody is waiting for. This is far better than each hop inventing its own timeout, because the whole chain agrees on one budget.

Metadata is key-value pairs sent alongside the message, the gRPC equivalent of HTTP headers. It carries things that are not part of the business payload: authentication tokens, request-tracing ids, locale hints. It travels separately from the typed message so your .proto stays focused on the domain.

Status codes are gRPC's small, fixed set of outcomes returned with every call — the analog of HTTP status codes but transport-independent. Every call ends with one.

StatusMeaningRough HTTP analog
OKSuccess200
INVALID_ARGUMENTClient sent bad input400
UNAUTHENTICATEDMissing/invalid credentials401
PERMISSION_DENIEDAuthenticated but not allowed403
NOT_FOUNDNo such entity404
DEADLINE_EXCEEDEDRan past the client's deadline504
UNAVAILABLEServer down / transient — safe to retry503
INTERNALServer bug500

The practical payoff: because UNAVAILABLE specifically signals a transient failure, a client can retry it with backoff, while it must not blindly retry INVALID_ARGUMENT (the input is bad and will fail every time). The status set makes that decision mechanical.

When gRPC beats REST — and when it does not

gRPC is not a universal replacement for REST; it wins decisively in a specific zone and loses in another. The lead-in rule: reach for gRPC between your own services, and keep REST (or GraphQL) at the edge facing browsers and third parties.

Where gRPC shines:

  • Internal service-to-service traffic. Dozens of microservices calling each other at high volume benefit most from the small payloads, multiplexing, and a strict contract that catches breakage at build time.
  • Low-latency, high-throughput paths. Binary encoding and HTTP/2 shave real milliseconds and bytes off hot paths.
  • Polyglot fleets. One .proto generates matching clients and servers for Go, Java, Python, Rust, and more, so teams in different languages cannot disagree about the interface.
  • Streaming needs. Live feeds, telemetry ingestion, and interactive sessions map naturally onto the four call types.

Where gRPC struggles:

  • Browsers. Browsers cannot speak raw gRPC (they cannot fully control HTTP/2 frames from JavaScript). You need a grpc-web shim plus a proxy that translates — extra moving parts many teams would rather avoid for a public API.
  • Human-unfriendliness. You cannot curl a gRPC endpoint and read the response, and you cannot eyeball the payload in browser dev tools. Debugging needs special tooling (grpcurl, reflection).
  • Public / third-party APIs. External developers expect JSON and REST; handing them a .proto and a binary protocol raises the barrier to adoption.
  • Caching at the HTTP layer. REST's GET responses slot neatly into CDNs and HTTP caches; gRPC calls (all POST-like) do not.

The common architecture is therefore a REST or GraphQL gateway at the public edge, translating to gRPC for the fast internal mesh behind it — which is exactly the gateway pattern later days build on.

Key takeaways

  • RPC models the network as calling a typed method; REST models it as acting on resources. gRPC is an RPC framework built on a mandatory schema plus HTTP/2.
  • Protocol Buffers is both the contract (a .proto IDL) and a compact binary wire format; protoc generates matching client and server code in many languages from that one file.
  • The wire carries field numbers, not names, which makes messages small and fast — and makes evolution safe: add fields with fresh numbers, never reuse or renumber, and reserved a removed number.
  • HTTP/2 multiplexing lets many calls share one connection without head-of-line blocking, and long-lived streams enable the four call types.
  • The four call types — unary, server-streaming, client-streaming, bidirectional — are chosen per method by which side sends a stream.
  • Every call carries a propagating deadline, out-of-band metadata, and ends in one of a small fixed set of status codes that make retry decisions mechanical.
  • gRPC wins for internal, low-latency, polyglot, streaming traffic; it loses at the browser-facing edge (needs grpc-web), for human debugging, and for public APIs — so pair it with a REST/GraphQL gateway.

Checklist

  • [ ] I can explain the difference between the RPC ("call a method") and REST ("act on a resource") mental models.
  • [ ] I can write a minimal .proto with a message, a service, and field numbers.
  • [ ] I can state why protobuf payloads are smaller and faster than JSON, and what is lost.
  • [ ] I know the field-number rules that keep messages backward and forward compatible (add fresh, never reuse, reserve removed).
  • [ ] I can explain HTTP/2 multiplexing and why gRPC requires HTTP/2.
  • [ ] I can name the four call types and identify which side streams in each.
  • [ ] I understand deadlines (and their propagation), metadata, and the role of status codes like UNAVAILABLE vs INVALID_ARGUMENT.
  • [ ] I can decide when gRPC is the right choice and when REST/GraphQL fits better.