Skip to content

Protocol Buffers Intro

Protocol Buffers (protobuf) is the language you use to describe your data and services, and the format they travel in. It plays two roles at once:

  • A schema language — a .proto file that declares your messages and services in a way any language can compile.
  • A wire format — the compact binary encoding those messages use when they cross the network.

In gRPC, protobuf is the contract. Everything — the type safety, the codegen, the efficiency — flows from it.

Here is the smallest interesting message:

syntax = "proto3";
message User {
int64 id = 1;
string name = 2;
string email = 3;
}

The = 1, = 2, = 3 are field numbers, and they are the most important idea in protobuf. On the wire, protobuf does not send field names. It sends the field number and the value. So name = "Ada" becomes roughly “field 2, string, Ada” — a couple of bytes of tag plus the value.

flowchart LR
  subgraph json["JSON on the wire"]
    j["{"id":1,"name":"Ada","email":"[email protected]"}
— keys repeated as text"]
  end
  subgraph pb["Protobuf on the wire"]
    p["[1→1][2→Ada][3→[email protected]]
— numbered tags + binary values"]
  end
JSON sends names every time; protobuf sends small numbered tags

Two consequences fall out of this one design choice:

  1. It’s compact. No repeated key names, no quotes, no whitespace — just tags and values.
  2. It’s evolvable. Because the number identifies the field, you can rename name to full_name in the .proto without breaking anyone — the wire only ever saw field 2. This is the foundation of safe schema evolution (a whole later lesson).

JSON is wonderful for public APIs — readable, universal, zero setup. But for high-volume service-to-service calls it has real costs that protobuf avoids:

JSONProtobuf
SizeKeys sent as text, every timeNumbered tags, binary values
Parse costReflective text parsingSchema-driven binary decode
SchemaOptional, drifts easilyMandatory, compiler-enforced
Type safetyRuntime, hope-for-the-bestGenerated types, compile-time
EvolutionConvention and courageField numbers make it structured

The trade-off is honesty in the other direction: protobuf is not human-readable on the wire, and you need the .proto to interpret bytes. That’s a fine deal for internal traffic and a poor one for a curl-friendly public API.

The discipline protobuf imposes is write the contract first. Before either team writes code, they agree on the .proto: these are the messages, these are the methods. Then codegen produces matching types on both sides. The schema can’t silently drift, because both the client and the server are generated from it.

What two roles does Protocol Buffers play?
What does protobuf actually send on the wire for a field?
Why can you rename a field in a .proto without breaking existing clients?
What is the main downside of protobuf versus JSON?