Part 5 · APIs & Communication
gRPC & Protocol Buffers for Beginners
Forty internal services, dozens of calls per page, JSON everywhere and contracts in a stale wiki. How gRPC and Protocol Buffers give services fast, typed, streaming calls — and the schema-evolution and load-balancing rules you must follow.
On this page
The Problem
Your company has grown from one app into 40 internal services. A service is a small program that does one job. Now a single "open product page" request starts dozens of calls between services: pricing, stock, reviews, recommendations and delivery estimates. Each call:
- sends JSON, a text format that is long and slow to parse (to parse is to read text and turn it into data),
- has a contract (the agreement about what data is sent) that lives only in a wiki page nobody updates,
- and breaks without warning when one team renames a field.
At millions of requests per second, the CPU time spent just encoding and decoding JSON becomes a real cost. And "contract drift" bugs keep coming. Contract drift means the code and the written contract slowly stop matching.
gRPC with Protocol Buffers was designed for exactly this. gRPC is a framework that lets one service call a function in another service over the network. Protocol Buffers (Protobuf) is a way to describe data and to turn it into small binary bytes. Together they give you fast, strongly-typed communication between services. "Strongly typed" means every field has a fixed type, like number or text. The contract is written as code.
The Core Idea
Think about two ways of sending instructions to a colleague.
Writing a letter in plain language (REST + JSON). It is flexible and anyone can read it. But it is long, the reader has to interpret it, and misunderstandings happen ("did you mean the delivery date or the order date?").
Filling in an agreed, numbered form (gRPC + Protobuf). Both sides have the same printed form. Field 1 is always the customer ID, field 2 is always the quantity. It is short, fast to process, and clear. If someone wants a new field, it is added to the shared form, and everyone knows about it.
gRPC also makes calling another service feel like calling a normal function in your code. A generated client object (a stub) hides the network work:
response = inventory.CheckStock(product_id="991", quantity=2)This is what RPC means: Remote Procedure Call. It is a call to a function that runs on another machine.
How It Works
Protocol Buffers: the contract and the format
You describe your data and your service in a .proto file. A message is a set of fields (like a record). A service lists the calls (rpc) that clients can make.
syntax = "proto3";package inventory.v1;service InventoryService { rpc CheckStock (CheckStockRequest) returns (CheckStockResponse); rpc WatchStock (WatchStockRequest) returns (stream StockUpdate); // server streaming}message CheckStockRequest { string product_id = 1; int32 quantity = 2;}message CheckStockResponse { bool available = 1; int32 units_in_stock = 2; string warehouse_id = 3;}The numbers (= 1, = 2, = 3) are field numbers. They are what is actually sent over the network ("the wire"), not the field names. This is one reason Protobuf is small.
Then a code generator (protoc, the Protobuf compiler, or tools like Buf) creates client and server code in your languages (Go, Java, Python, TypeScript, C#, Rust and more):
- inventory.protothe contract
- protoc / Bufcode generator
- Go server · Java client · Python client
Every team uses the same contract. In compiled languages the compiler (the tool that turns code into a program) catches mismatches before anything runs.
Why Protobuf is smaller and faster than JSON
The same data in two formats:
- No field names are repeated in each message. Only small field numbers are sent.
- Numbers are packed tightly (small numbers use fewer bytes, called varint encoding).
- Reading binary is much faster than reading text. Binary means raw bytes, not readable characters.
The trade-off: you cannot read it by eye. You need tools like grpcurl (like curl, but for gRPC) or Postman to look at it.
Evolving schemas safely
APIs change over time. Protobuf supports backward compatibility (new code can read old data) and forward compatibility (old code can read new data), if you follow a few rules:
- ✅ Adding new fields is safe. Old clients ignore fields they do not know. New clients see default values (such as 0 or an empty string) when an old server does not send the field.
- ✅ Removing a field is OK, but mark its number and name as
reserved(the compiler then refuses to let anyone use them again):Protobufmessage CheckStockResponse { reserved 3; reserved "warehouse_id"; bool available = 1; int32 units_in_stock = 2;} - ❌ Never change a field's number.
- ❌ Never reuse an old field number for something new. Old data or old clients will read it wrongly.
- ❌ Do not change a field's type (like
int32tostring). - Put a version in the package name (
inventory.v1) and createv2for breaking changes.
gRPC runs on HTTP/2
gRPC sends its calls over HTTP/2 (lesson 3), a newer version of HTTP that sends binary data and can carry many requests at once. This gives gRPC:
- Multiplexing: many calls at the same time over one connection, without waiting for each other.
- Binary framing (messages are cut into small binary pieces) and header compression (repeated header data is made smaller).
- Streaming in both directions: data can keep flowing as a series of messages.
Four kinds of calls
- •one request → one response
- •one request → many responses
- •many requests → one response
- •both sides stream independently
Deadlines and cancellation
- Service A300 ms left
- Service Bspends 100 ms → 200 ms left
- Service Cspends 150 ms → 50 ms left
- Deadline hitseveryone stops — no wasted work
In gRPC, every call can carry a deadline: "I need an answer within 300 ms". A deadline is the latest time by which the answer must arrive. It travels with the request to downstream services (the services that your service calls in turn). If service A calls B with 300 ms left, and B spends 100 ms, B's call to C carries the remaining ~200 ms. (Your code must pass the call's context to the next call for this to work.) When the deadline passes, everyone stops working on that request.
This prevents wasted work. It also helps stop cascading failures (Part 7), where one slow service makes the services that call it slow too. Always set deadlines. gRPC has no deadline by default, so a call could wait forever.
gRPC also has its own status codes: OK, NOT_FOUND, INVALID_ARGUMENT, DEADLINE_EXCEEDED, UNAVAILABLE, PERMISSION_DENIED, RESOURCE_EXHAUSTED (rate limited) and more. They are similar in purpose to HTTP status codes.
Interceptors (middleware)
Interceptors are pieces of code that run before and after every call. They are the natural place for:
- authentication (checking tokens),
- logging and metrics,
- distributed tracing (following one request across many services, Part 8),
- retries with backoff (trying again after a growing wait, Part 7).
The load-balancing pitfall
A load balancer shares requests between several servers. gRPC keeps long-lived HTTP/2 connections and sends many requests over each one. This causes a problem with L4 load balancers (lesson 12). An L4 balancer works at the connection level. It picks a server once per connection and does not look at the requests inside. An L7 balancer reads each request.
- •Client A ═══► Server 1 (all of A's calls)
- •Client B ═══► Server 1 (all of B's calls)
- −Servers 2 and 3 sit idle
- •Each call on the shared HTTP/2 connection is balanced
- +Load spreads evenly
Once a connection is assigned, every request on it goes to the same server, so load becomes very uneven.
Fixes:
- An L7 load balancer / proxy that understands HTTP/2 and balances per request (Envoy, NGINX, a service mesh, or a cloud L7 LB with gRPC support).
- Client-side load balancing: the gRPC client knows all server addresses (from DNS or service discovery) and spreads requests itself.
gRPC in the browser
Browsers do not give JavaScript the low-level HTTP/2 control that gRPC needs. Options:
- gRPC-Web, a browser-friendly version of gRPC. A proxy (such as Envoy) translates it to real gRPC.
- Connect, a newer protocol that works with gRPC and also works over plain HTTP with JSON.
- Most commonly: REST or GraphQL for browsers and mobile, gRPC between backend services.
REST vs gRPC
| REST + JSON | gRPC + Protobuf | |
|---|---|---|
| Format | Text (JSON), human-readable | Binary, compact |
| Contract | Optional (OpenAPI) | Required (.proto), code generated |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/2 |
| Streaming | Limited (SSE / WebSockets separately) | Built in (4 modes) |
| Browser support | ✅ Native | 🟡 Needs gRPC-Web/Connect |
| Debugging | ✅ curl, browser | 🟡 Needs tools (grpcurl, Postman) |
| Caching via HTTP/CDN (a CDN is a network of servers that cache content near users) | ✅ Easy | ❌ Not really |
| Best for | Public APIs, web/mobile clients | Internal service-to-service, high performance, streaming |
Alternatives
- Apache Thrift: created at Facebook. It has a similar idea, with its own data format and RPC framework.
- Apache Avro: popular in data pipelines and Kafka (a system for streams of events). The schema is stored together with the data.
- JSON-RPC: simple RPC that sends calls as JSON.
- MessagePack / CBOR: compact binary encodings of JSON-like data, without schemas.
Trade-offs
- ✅ Fast and compact, with strong contracts and generated code, built-in streaming, and deadlines and cancellation.
- ✅ Polyglot: one contract works for many programming languages.
- ❌ Not readable by people, so debugging needs tools.
- ❌ Limited browser support and no easy HTTP/CDN caching.
- ❌ Load balancing needs L7 or client-side balancing.
- ❌ Schema discipline required: careless changes to field numbers break clients.
- ❌ Learning curve and build tooling (code generation, managing
.protofiles across teams).
When not to use gRPC: public APIs for third-party developers (REST is easier for them), simple apps with a few endpoints, or when your team and infrastructure do not support HTTP/2 end to end.
In the Real World
Google created gRPC based on its internal RPC system (called Stubby), which it had used for years for a huge number of calls between services. It released gRPC as open source in 2015. Today gRPC is a project of the Cloud Native Computing Foundation (CNCF).
Kubernetes and etcd. etcd is the database that stores the state of a Kubernetes cluster. It offers a gRPC API. Kubernetes uses gRPC in key places, such as the interface (the CRI) between the kubelet (the agent on each node) and the container runtime (the program that runs containers). Envoy's configuration APIs (xDS) are gRPC as well.
Dropbox's migration to gRPC. Dropbox wrote about moving its many internal services to a gRPC-based framework (called Courier), and gained one clear RPC layer with deadlines, tracing and security built in.
Microservice platforms. Many companies with large microservice estates (in ride-hailing, fintech, streaming and e-commerce) use gRPC internally, while exposing REST or GraphQL APIs to their mobile apps and partners. This is the "gRPC inside, REST/GraphQL outside" pattern.
Interview Questions
Q1Why would you use gRPC instead of REST between services?
Protobuf messages are small binary data, so they are cheaper to send and read. The typed contract is required, and clients are generated in every language. HTTP/2 multiplexing and streaming are built in. Deadlines pass along through chains of calls. REST is easier for public APIs that browsers use.
Q2How do you evolve a Protobuf schema without breaking clients?
Add new fields with new numbers, because old readers ignore them. Never change or reuse a field number, and never change a field's type. Mark removed numbers and names as reserved. Put a version in the package name and create v2 for breaking changes.
Q3What problem do gRPC and L4 load balancers have together?
gRPC sends many calls over one long-lived HTTP/2 connection, and L4 balancers balance whole connections. So each client's traffic stays on one server, and the load becomes very uneven. Use an L7 proxy that balances each request, or client-side load balancing.
Q4Why are deadlines important in gRPC?
Without a deadline, a call can wait forever and hold resources in the services that called it. A deadline sets a total time limit that is passed downstream. Every service knows how much time is left, and all of them stop work once the caller has given up.
Q5Can browsers call gRPC services directly?
Not native gRPC, because browsers do not give the HTTP/2 control it needs. You can use gRPC-Web through a proxy like Envoy, or the Connect protocol. Most often, teams use REST or GraphQL at the edge and gRPC inside.
Key Takeaways
- gRPC makes remote calls feel like local function calls. Protocol Buffers define the contract and a compact binary format.
- Code is generated from
.protofiles, so every team shares one typed contract. - Evolve schemas safely: add fields freely, never change or reuse field numbers, and use
reserved. - gRPC runs on HTTP/2 and supports unary and streaming calls. Always set deadlines, which propagate through the chain.
- Use L7 or client-side load balancing. Use gRPC mainly between backend services, and REST or GraphQL for browsers and public APIs.
Further Reading
- The official gRPC documentation ("Introduction" and "Core concepts")
- The official Protocol Buffers documentation, including its best practices for schema changes
- The gRPC blog article "gRPC Load Balancing"
- Designing Data-Intensive Applications by Martin Kleppmann (chapter 4, "Encoding and Evolution")
- The System Design Primer on GitHub (sections "Remote procedure call (RPC)" and "RPC and REST calls comparison")
- Dropbox's engineering blog post on migrating to gRPC ("Courier")
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series