Messages & Fields
Anatomy of a message
Section titled “Anatomy of a message”A message is a named group of typed fields. Each field has three parts: a type, a name, and a field number.
syntax = "proto3";package user.v1;
message User { int64 id = 1; string name = 2; string email = 3; bool verified = 4;}The name (name, email) is for humans reading the .proto and for the generated code. The number (= 1, = 2) is what actually travels on the wire. That split is the single most important thing to internalize about protobuf.
The wire only sees numbers
Section titled “The wire only sees numbers”When protobuf encodes a message, each field becomes a tag — the field number combined with a wire type — followed by the value. Field names never leave the .proto.
flowchart LR field["name = "Ada" (field #2)"] --> tag["tag: field 2, type length-delimited"] tag --> val["value: 3 bytes 'Ada'"] val --> bytes["on the wire: a few bytes total"]
This has two big payoffs:
- Compactness. The string
"verified"never travels; the number4does. Messages stay small no matter how descriptive your field names are. - Renaming is free. Change
nametofull_namein the.protoand every client still interoperates — the wire only ever carried field2. Names are documentation; numbers are the contract.
Field numbers are not arbitrary
Section titled “Field numbers are not arbitrary”You choose the numbers, and the choice has performance and safety consequences.
- 1–15 encode in a single byte (tag = number + wire type packed together). Reserve them for the fields present in almost every message.
- 16–2047 take two bytes. Fine for less common fields.
- 19000–19999 are reserved by protobuf itself — you cannot use them.
- The maximum is 536,870,911 (2^29 − 1), which you will never reach.
Never reuse a number — reserve it
Section titled “Never reuse a number — reserve it”The one rule that prevents the worst protobuf bug: when you delete a field, never let its number be reused. If an old client still sends field 3 as a string and a new server reassigns 3 to an int64, the bytes get misinterpreted — a silent, corrupting failure.
Protobuf gives you reserved to lock a retired number (and optionally its old name) so the compiler refuses to reuse it:
message User { reserved 3; // the old 'email' field number — never reuse reserved "email"; // and the old name, to catch mistakes
int64 id = 1; string name = 2; bool verified = 4;}Now anyone who tries to add a new field 3 gets a compile error instead of a production incident. We return to this discipline in depth in the Evolving Schemas lesson — but the habit starts here: delete a field, reserve its number.