Search Tech Journey

Find topics, journeys and posts

6-month learning plan58 / 130
back to blog
backend apisintermediate 15m read

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)


(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:

AspectREST + JSONgRPC + Protobuf
Sample payload{"user_id":42,"name":"Alice","active":true}08 2A 12 05 41 6C 69 63 65 18 01
Bytes4211
TransportHTTP/1.1 or HTTP/2HTTP/2 required
SchemaOpenAPI (optional).proto file (mandatory)
StreamingSSE / websockets (bolt-on)First-class (4 modes)
Browser supportNativeNeeds gRPC-Web proxy
Human-readable?YesNo (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 2A means "field #1, varint 42").
  • Varints — small integers take 1 byte, big ones grow. 0 and 1 are 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 protos

Create 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 appear

server.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.py

What to observe:

  1. The unary call returns instantly; streaming delivers 5 replies with 200ms between each.
  2. wire bytes: 9 bytes — that's the full binary payload for {name:"Alice",count:42}. JSON of the same is ~26 bytes.
  3. Kill the server mid-stream — client raises _MultiThreadedRendezvous with StatusCode.UNAVAILABLE. gRPC surfaces transport errors as standard status codes.
  4. Set timeout=0.001 on the unary call — you get StatusCode.DEADLINE_EXCEEDED. That's the deadline mechanism.
  5. Delete the generated _pb2.py files, 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 added string age_range = 3; a month later. Old clients started returning garbled strings for age_range. Fix: always use reserved 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=None on 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 (like curl for gRPC), debugging in prod feels like flying blind. Fix: enable reflection in non-prod, use tools like grpcurl, evans, or ghz for 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

  1. Name three concrete reasons gRPC is faster than REST/JSON on the wire.
  2. What are the four gRPC call modes, and give one real use case for each.
  3. Why is renumbering a Protobuf field tag a data-corruption bug waiting to happen?
  4. What's the difference between a timeout and a deadline, and why does gRPC prefer deadlines?
  5. 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:

  1. What is gRPC? (one sentence, no jargon)
  2. When would you use it? (one concrete example)
  3. When would you NOT use it? (one anti-pattern)

What comes next


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 →
  1. 055HTTP Fundamentals — Verbs, Status Codes, Headers, Caching
  2. 056REST API Design — Resources, Versioning, Idempotency
  3. 057GraphQL — Schema, Resolvers, N+1, When to Pick It
  4. 059AuthN & AuthZ — OAuth 2.0, OIDC, JWT