S058 · gRPC & Protobuf — When RPC Wins
Binary + typed = fast.
Module M06: Backend & APIs · Session 58 of 130 · Track: SYS ⚡
What you'll be able to do after this session
- Explain gRPC and Protobuf to a friend in one minute, out loud, no notes.
- Recognise it when you see it in real code / systems / papers.
- Complete the hands-on exercise at the end without looking up help.
Prerequisites
If you can't answer these in 30 seconds each, redo the linked session first:
Pre-read (skim before the session)
- 🎥 Intuition (5–15 min) · What is gRPC? — IBM Technology — 9-minute whiteboard intro; excellent framing.
- 🎥 Deep dive (30–60 min) · gRPC Crash Course — Hussein Nasser — the deep-dive on HTTP/2, streaming modes, and Protobuf.
- 🎥 Hands-on demo (10–20 min) · Build a gRPC service in Python — 15 min — click-along stub generation and unary + streaming.
- 📖 Canonical article · gRPC official — Introduction to gRPC — the authoritative overview.
- 📖 Book chapter / tutorial · Protobuf language guide (proto3) — 30-minute skim; you'll refer back to this often.
- 📖 Engineering blog · Netflix — Practical API Design at Netflix (Part 2: gRPC/Federation) — how Netflix uses Protobuf FieldMask for partial mutations.
(a) Intuition · 5 min
The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.
In one sentence: Binary + typed = fast.
REST/JSON is like sending letters written in English — human-readable, universal, but verbose ("Dear server, please find enclosed the following field: name, and its value is Alice, best regards"). gRPC is like sending Morse code between two operators who have the same codebook — tiny binary payloads over a persistent connection, both sides pre-compiled from the same schema (a .proto file). Result: 5-10× smaller messages, ~5× faster serialization, and a strongly-typed contract that generates client + server stubs in 20+ languages.
The problem gRPC was built to solve (at Google, internally, since ~2001, open-sourced 2015): microservices talking to each other 100,000 times per second where every millisecond and every byte matters. REST/JSON's overhead dominates at that scale — parsing JSON allocates memory, string keys repeat in every message, and text is 3–10× larger than a good binary encoding.
The three pieces: (1) Protobuf — a schema language (message User { string name = 1; int32 id = 2; }) that compiles to code AND to a compact binary wire format. Field numbers, not names, go on the wire. (2) gRPC — the RPC framework built on Protobuf + HTTP/2 that gives you client/server stubs, streaming, deadlines, and interceptors. (3) HTTP/2 — the underlying transport that supports multiplexing and streaming for free.
Forever gotcha: never reuse or renumber Protobuf field tags. Field number 2 = int32 id is the wire contract. If you delete the field and later add string display_name = 2, existing clients will read old id bytes as garbled strings. Mark deleted fields with reserved 2; and move on.
(b) Visual walkthrough · 15 min
Diagrams, worked examples, step-by-step visuals.
REST/JSON vs gRPC/Protobuf — same message on the wire:
| Aspect | REST + JSON | gRPC + Protobuf |
|---|---|---|
| Sample payload | {"user_id":42,"name":"Alice","active":true} | 08 2A 12 05 41 6C 69 63 65 18 01 |
| Bytes | 42 | 11 |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/2 required |
| Schema | OpenAPI (optional) | .proto file (mandatory) |
| Streaming | SSE / websockets (bolt-on) | First-class (4 modes) |
| Browser support | Native | Needs gRPC-Web proxy |
| Human-readable? | Yes | No (need protoc to decode) |
The four gRPC call modes — pick the shape that fits:
A worked example — the .proto:
syntax = "proto3";
package shop;
service Orders {
rpc GetOrder (GetOrderRequest) returns (Order); // unary
rpc ListOrders (ListOrdersRequest) returns (stream Order); // server stream
rpc UploadEvents (stream Event) returns (UploadSummary); // client stream
rpc Chat (stream ChatMsg) returns (stream ChatMsg); // bidi
}
message GetOrderRequest { string id = 1; }
message Order {
string id = 1;
int64 customer_id = 2;
int64 amount_cents = 3;
Status status = 4;
enum Status { PENDING = 0; PAID = 1; CANCELLED = 2; }
reserved 5, 6; // do not reuse these!
reserved "old_field";
}You run protoc --python_out=. --grpc_python_out=. shop.proto and out pops shop_pb2.py (data classes) + shop_pb2_grpc.py (client + server stubs). Same command with --go_out=. gives you Go. Same with --java_out=. gives you Java. Cross-language contracts for free.
How Protobuf achieves smallness:
- Field names are NOT on the wire; only field numbers (
08 2Ameans "field #1, varint 42"). - Varints — small integers take 1 byte, big ones grow.
0and1are 1 byte each. - Default values are omitted entirely on the wire.
- No whitespace, no quotes, no commas.
Deadlines, not just timeouts — every gRPC call carries a deadline (absolute time) that propagates through the whole call graph. If the client sets deadline = now + 500ms, and service A calls service B, service B sees the remaining deadline. Prevents cascading timeouts (the "queue everything and hope" antipattern).
Interceptors — the middleware of gRPC. You attach them client-side (retries, auth, tracing) or server-side (logging, auth, rate-limiting). One codepath, all your cross-cutting concerns.
(c) Hands-on · 20 min
Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.
Build a real gRPC server + client end-to-end in ~70 lines.
python -m venv .venv && source .venv/bin/activate
pip install grpcio==1.66.1 grpcio-tools==1.66.1
mkdir -p protos && cd protosCreate protos/greeter.proto:
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc StreamGreetings (HelloRequest) returns (stream HelloReply);
}
message HelloRequest { string name = 1; int32 count = 2; }
message HelloReply { string message = 1; }Generate code:
cd ..
python -m grpc_tools.protoc -I=protos --python_out=. --grpc_python_out=. protos/greeter.proto
ls # greeter_pb2.py greeter_pb2_grpc.py appearserver.py:
import grpc, time, asyncio
from concurrent import futures
import greeter_pb2, greeter_pb2_grpc
class Greeter(greeter_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
print(f"unary request: name={request.name!r}")
return greeter_pb2.HelloReply(message=f"hello {request.name}")
def StreamGreetings(self, request, context):
for i in range(request.count):
if context.is_active() is False:
return
yield greeter_pb2.HelloReply(message=f"greeting #{i+1} to {request.name}")
time.sleep(0.2)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
greeter_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port("[::]:50051")
server.start()
print("gRPC server on :50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()client.py:
import grpc, greeter_pb2, greeter_pb2_grpc
def main():
with grpc.insecure_channel("127.0.0.1:50051") as ch:
stub = greeter_pb2_grpc.GreeterStub(ch)
# 1. Unary call with a 2-second deadline
resp = stub.SayHello(greeter_pb2.HelloRequest(name="Alice"), timeout=2.0)
print("unary:", resp.message)
# 2. Server-streaming call — get an iterator, loop over it
req = greeter_pb2.HelloRequest(name="Bob", count=5)
for reply in stub.StreamGreetings(req):
print("stream:", reply.message)
# 3. Show message size on the wire
one = greeter_pb2.HelloRequest(name="Alice", count=42)
print("wire bytes:", len(one.SerializeToString()), "bytes")
print("wire hex: ", one.SerializeToString().hex())
if __name__ == "__main__":
main()Run in two terminals:
# terminal 1
python server.py
# terminal 2
python client.pyWhat to observe:
- The unary call returns instantly; streaming delivers 5 replies with 200ms between each.
wire bytes: 9 bytes— that's the full binary payload for{name:"Alice",count:42}. JSON of the same is ~26 bytes.- Kill the server mid-stream — client raises
_MultiThreadedRendezvouswithStatusCode.UNAVAILABLE. gRPC surfaces transport errors as standard status codes. - Set
timeout=0.001on the unary call — you getStatusCode.DEADLINE_EXCEEDED. That's the deadline mechanism. - Delete the generated
_pb2.pyfiles, rerun — everything breaks. gRPC/Protobuf lives and dies by codegen.
Try this modification: add a client interceptor that logs every outgoing call with its RPC method name and duration. Then add a server interceptor that rejects requests without an authorization metadata header. These two together give you the observability + auth foundation of a real microservice — with < 30 lines of extra code.
(d) Production reality · 10 min
How this shows up in real systems, gotchas, war stories, common bugs.
gRPC is the default for internal service-to-service traffic at Google, Netflix, Square, Dropbox, Uber, Cloudflare, and Cockroach Labs. Kubernetes itself uses Protobuf on the wire (client-go can serialize as either JSON or Protobuf; Protobuf wins at scale). Envoy, Istio, Linkerd, and every modern service mesh natively speak gRPC because it rides on HTTP/2. If you're building a new microservice architecture in 2025, gRPC + Protobuf is the safe default over REST for east-west traffic.
War stories:
- The renumbered-field disaster. Team deleted
int32 age = 3;and addedstring age_range = 3;a month later. Old clients started returning garbled strings forage_range. Fix: always usereserved 3;when deleting fields — protoc will refuse to reuse the number. - The load-balancer that couldn't. gRPC uses long-lived HTTP/2 connections. A classic L4 load balancer (TCP round-robin) pinned every client to one backend forever — no rebalancing when new pods came up. Fix: use an L7 gRPC-aware balancer (Envoy, Linkerd, or client-side lookaside like xDS) or enable client-side round-robin over a headless service.
- The infinite deadline. Someone set
timeout=Noneon a client because "it's just an internal call." Backend hung on a lock, workers pinned, cascading failure across 4 services. Fix: every gRPC call must have a deadline. Enforce it in an interceptor that rejects calls without one. - The message-size cliff. Default gRPC max message size is 4 MB. Someone tried to send a 5 MB thumbnail →
RESOURCE_EXHAUSTED. Fix: don't ship blobs over gRPC — use pre-signed URLs to S3. If you must, use client streaming and chunk it. - The browser blocker. Team built a browser-facing feature on raw gRPC → doesn't work; browsers can't do arbitrary HTTP/2 trailers. Fix: gRPC-Web via an Envoy translation proxy, or just use JSON/REST for browser edges and gRPC internally.
- The observability gap. gRPC binary messages aren't grep-able. If you don't set up server reflection and use
grpcurl(likecurlfor gRPC), debugging in prod feels like flying blind. Fix: enable reflection in non-prod, use tools likegrpcurl,evans, orghzfor load testing.
How top teams handle it: .proto files live in a shared repo (protos/ or Buf's Schema Registry), CI runs buf breaking on every PR to reject breaking schema changes automatically, servers register with a service registry for discovery, deadlines propagate end-to-end, and metrics (RPC method, status code, latency histogram) are collected by an interceptor.
Gotcha to memorise: default values are indistinguishable from unset in proto3. int32 quantity = 1; — if the field is 0, is that "explicitly zero" or "never set"? To distinguish, use a wrapper type (google.protobuf.Int32Value) or an explicit "presence" oneOf. This bites everyone once; don't be the second.
(e) Quiz + exercise · 10 min
- Name three concrete reasons gRPC is faster than REST/JSON on the wire.
- What are the four gRPC call modes, and give one real use case for each.
- Why is renumbering a Protobuf field tag a data-corruption bug waiting to happen?
- What's the difference between a timeout and a deadline, and why does gRPC prefer deadlines?
- What are the two hard limits that make raw gRPC unusable directly from browsers?
Stretch (connects to S056 — REST): You're building a mobile app + a web app + 30 internal microservices. Which endpoints should be REST, GraphQL, and gRPC — and why? Sketch the boundary and justify each choice with one sentence.
Explain-out-loud test
If you can't teach these 3 things to a friend without notes, redo the session:
- What is gRPC? (one sentence, no jargon)
- When would you use it? (one concrete example)
- When would you NOT use it? (one anti-pattern)
What comes next
- Next session (S059): AuthN & AuthZ — OAuth 2.0, OIDC, JWT
- Previous (S057): GraphQL — Schema, Resolvers, N+1, When to Pick It
- Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.
More from M06 · Backend & APIs
all modules →