Protocol Buffers

Also known as: Protobuf

On this page 7

Protocol Buffers, commonly called Protobuf, is a language-neutral, schema-based binary serialization format for structured data. Messages are defined in .proto files, a compiler generates code for many programming languages, and the resulting bytes are compact, fast to parse and designed for backward and forward compatibility.

Among data formats, Protocol Buffers occupies the space of typed messages exchanged between programs: remote procedure calls, event streams, configuration and storage of small records. It is the default message format of gRPC and is widely used in microservice architectures, mobile clients and internal data pipelines.

How Protocol Buffers encodes data

Protocol Buffers encodes each message as a sequence of key-value records, where the key combines a field number and a wire type. Field names are not transmitted; only the numbers declared in the schema identify fields, which is the main reason the encoding is compact.

Integers use variable-length encoding (varint), in which small values take fewer bytes: a value below 128 fits in one byte. Signed values that are often negative should use sint32 or sint64, which apply ZigZag encoding, because a plain negative int32 always occupies ten bytes.

The following table lists the wire types that determine how the parser reads each field.

Wire typeUsed forSize on the wire
VARINT (0)int32, int64, uint, bool, enum, sint1-10 bytes
I64 (1)fixed64, sfixed64, doubleAlways 8 bytes
LEN (2)string, bytes, nested messages, packed repeatedLength prefix plus payload
I32 (5)fixed32, sfixed32, floatAlways 4 bytes
Protocol Buffers wire types

Because every field carries its number and wire type, a parser can skip unknown fields it does not recognize. This property underpins schema evolution: an older program can read messages produced by a newer one without failing.

Schema definition and code generation

A Protocol Buffers schema is written in the proto language, where each message lists typed fields with unique numbers. A minimal definition looks like message Order { int64 id = 1; string customer = 2; repeated Item items = 3; }.

The compiler protoc, or an equivalent build tool, generates classes or structs with serializers for languages such as C++, Java, Go, Python, C# and others. Generated code provides compile-time type checking and avoids hand-written parsing logic. The current syntax is proto3, and newer releases organize features through editions instead of syntax versions.

ElementPurposePractical note
Field numberIdentifies a field on the wire1-15 encode in one tag byte
repeatedDeclares a list of valuesScalars are packed by default in proto3
oneofAt most one of several fields is setModels variants and unions
mapKey-value collectionOrdering is not guaranteed
optionalTracks whether a scalar was setDistinguishes zero from absent
reservedBlocks reuse of retired numbers and namesPrevents silent data corruption
Key schema elements

Field numbers range from 1 to about 536 million, excluding the block 19000-19999 reserved for the implementation. Numbers 1 to 15 use a single byte for the tag, so they are best assigned to the most frequent fields.

Schema evolution rules

Protocol Buffers supports evolving schemas without breaking deployed readers, provided a small set of rules is followed. The rules exist because only field numbers and wire types, not names, are stored in the data.

  • Never change the number of an existing field.
  • Add new fields with new numbers; old readers ignore them.
  • Reserve removed numbers and names so they are not reused later.
  • Change types only between compatible encodings, such as int32 and int64, with care for truncation.
  • Avoid required semantics; proto3 removed required fields because they made evolution brittle.

In proto3, a scalar field that is not set reads as its default value, such as 0 or an empty string. Code that must distinguish “zero” from “not provided” should declare the field optional or use a wrapper message.

Protocol Buffers compared with other data formats

Protocol Buffers trades human readability for compactness and type safety. Compared with JSON, payloads are typically several times smaller and faster to parse, although the exact gain depends on the data and the libraries.

FormatEncodingSchemaTypical strength
Protocol BuffersBinary, row-orientedRequired, compiledTyped RPC and event messages
JSONTextOptionalReadability, browser support
Apache AvroBinary, row-orientedRequired, often sent with dataData files and schema registries
MessagePackBinaryNoneCompact JSON-like documents
ParquetBinary, columnarEmbeddedAnalytical scans over large datasets
Protocol Buffers and related formats

Unlike Apache Avro, a Protobuf message cannot be decoded meaningfully without its schema, because field names are absent. Unlike Parquet, Protobuf is row-oriented and is not designed for columnar analytics over billions of records.

When to use it and common pitfalls

Protocol Buffers fits best where both ends are controlled programs that exchange many small, well-defined messages: service-to-service calls, mobile APIs, event buses and internal storage of records. It is a weaker fit for ad hoc data exchange with people or for public web APIs consumed directly in browsers.

PitfallConsequencePrevention
Reusing field numbersOld data decoded into wrong fieldsMark retired numbers reserved
Large single messagesHigh memory use during parsingKeep messages small, stream lists
Floating schema copiesServices drift out of syncCentral schema repository and linting
Defaults mistaken for dataMissing values read as zeroUse optional fields or wrappers
Pitfalls when adopting Protocol Buffers

Teams usually keep .proto files in a shared repository with automated breaking-change checks in continuous integration, so incompatible edits are caught before deployment. Messages above a few megabytes are better split, since common implementations impose default size limits and parse whole messages into memory.

Put simply

Protocol Buffers is like a shared form with numbered boxes. Both sides agree in advance that box 1 holds the order number and box 2 holds the customer name. When they send a filled form, they only send the box numbers and the contents.

Because the labels never travel, the messages are small and quick to read. Both sides keep a copy of the blank form, and new boxes can be added later without confusing anyone who still uses the old version.

In practice

A ride-hailing platform runs about 40 internal services that exchange trip, driver and pricing data. The team originally used JSON over HTTP, and profiling showed that serialization and parsing consumed a notable share of CPU on busy services.

The team defines the messages in Protocol Buffers, stores the .proto files in one repository and generates Go and Java code during the build. A linting step rejects changes that reuse or renumber fields. Location updates from drivers shrink from roughly 400 bytes of JSON to under 100 bytes.

After migration, CPU use on the dispatch service drops, mobile data use falls and a new field for estimated tolls is rolled out gradually, because older services simply ignore the unknown field until they are updated.