API changes

Protobuf breaking changes: what survives on the wire

6 min read

A REST API changes when a JSON shape changes, and most of that shape is visible in the response you can read in a browser. A gRPC API changes when a .proto file changes, and Protocol Buffers’ binary wire format has its own rules about what a client can tolerate that have nothing to do with what the field names say. Two edits that look equally small in a diff, renumbering a field versus adding one, land on opposite sides of a line that breaking changes draws in general: one is invisible to every existing client, the other breaks all of them at once. Telling protobuf breaking changes apart from safe ones means reading the wire format’s own rules, not guessing from how the change reads in a .proto diff.

Why does field numbering matter more than field naming in Protobuf?

Because the wire format encodes fields by number, not by name. The generated code in every language reads and writes those numbers; the field name email in your .proto file is a convenience for humans that never touches the binary bytes sent over the network. Renaming a field, email to email_address, is safe on the binary wire as long as the number stays the same, which surprises engineers used to REST, where a renamed JSON key is exactly the kind of change that breaks a client. The exception is the same REST case: the ProtoJSON and text formats serialize the name, so a rename breaks JSON transcoding (a grpc-gateway, for example), text-format files and field masks. Renumbering that same field, keeping the name but changing 1 to 7, is exactly backwards: invisible in a code review that only shows names, and it corrupts every message a client sends or receives from that point on.

ChangeSafe on the wireWhy
Rename a field, keep its numberBinary yes, JSON and text noBinary encoding uses the number; ProtoJSON and text format use the name
Change a field’s numberNoEvery existing message is now misread as the wrong field
Add a new field with a new numberYesOld clients ignore fields they don’t recognize
Remove a field, reuse its old number for something elseNoOld data decodes into the wrong new field
Change a field’s type incompatibly (e.g. int32 to string)NoThe wire encoding differs per type

What makes removing a field different from removing one in a REST JSON response?

The number becomes radioactive. Protobuf’s own guidance recommends marking a removed field’s number reserved rather than letting it be reused, because reuse is where the real damage happens: a client still running last month’s generated code sends a message using the old field’s number for the old meaning, and the server, now expecting that number to mean something else, silently misinterprets the data instead of rejecting it outright. REST has no equivalent trap, because a removed JSON key just stops appearing; there’s no way for an old client’s request to be quietly reinterpreted as something else. A .proto file with reserved 4, 9, 12; at the top of a message is a permanent scar, and that’s the point: it stops the number from being handed to a new field by someone who didn’t know its history.

message Invoice {
  reserved 4; // was `legacy_customer_id`, removed 2026-06-01
  reserved "legacy_customer_id"; // the name too, for JSON/text
  string customer_id = 5;
  string status = 6;
}

Does adding a field ever require a changelog entry at all?

Usually not a breaking-change entry, but often a plain one, because “safe on the wire” and “invisible to a reader who cares” are different claims. Adding a field to a response message costs nothing structurally, old clients decode the message and ignore the new field automatically. But a caller building a new integration against that service has no way to know the field exists unless someone tells them, since nothing about a successful build or a passing test surfaces a new optional field. API changelog covers what an additive entry owes readers in general; the gRPC-specific reason to still write one is that there’s no equivalent of browsing a REST response in a debugger to notice a new key showed up.

How is this different from what GraphQL callers deal with?

The rules for additions match, but the exposure differs. GraphQL schema deprecation covers a model where a client only receives fields it explicitly requests, which makes additive changes essentially risk-free and removals the only real danger. gRPC clients, by contrast, receive whatever the server sends and decode all of it against their own compiled copy of the schema; a client’s exposure isn’t bounded by what it asked for, only by what its generated code knows how to read. That difference matters for changelog writing: a GraphQL entry can reasonably assume clients are shielded from fields they didn’t request, and a gRPC entry can’t make that assumption at all.

Does versioning a gRPC service work the same way as REST’s /v1/, /v2/?

The mechanism is different even when the intent is the same. What is v1 and v2 in a REST API covers versioning as parallel URL paths serving different contracts; gRPC services typically version through the package name in the .proto file itself, payments.v1.InvoiceService becoming payments.v2.InvoiceService, which changes the fully-qualified service name a client dials rather than a URL segment it requests. Both approaches solve the same problem, letting an old contract keep working while a new one exists, but a team moving from a REST background often looks for a version number in the wrong place and misses that the package declaration is doing that job.

What should a gRPC changelog entry actually name?

The message, the field number, and whether it’s additive or a removal requiring migration, in that order of importance to a reader deciding whether to act. “Added shipping_address (field 8) to Order” tells an integrator everything needed to update generated code and start using it. “Reserved field 4 on Invoice, legacy_customer_id is gone” tells them to check whether anything in their codebase still reads that field, which a REST-style “removed a field from the response” note doesn’t communicate with the same urgency, because REST removals just return less data while Protobuf field reuse actively corrupts it.

FAQ

Can a field’s type ever be changed without breaking the wire format? Only within specific compatible groups Protobuf documents, like widening int32 to int64 in some cases. Treat any type change as breaking unless you’ve checked it against Protobuf’s own compatibility table; assuming compatibility by analogy with a language’s type system is how this goes wrong.

Does deprecating a field in Protobuf work like GraphQL’s @deprecated directive? Similarly: Protobuf supports a [deprecated = true] field option that tooling can surface. Neither is enforced: a GraphQL server still answers a query for a deprecated field, and a protobuf client still encodes one. Both are advisory and need the same changelog backup.

Is renumbering ever safe if you control every client? In a fully closed system, in principle, but it removes the entire safety property that field numbers exist to provide, and “we control every client” is a claim that stops being true the moment a build is cached, a deploy is delayed, or a client is added that nobody remembered. Reserve the number instead of reusing it, even internally.

Do gRPC services need a changelog page the way a public REST API does? Only if external teams consume them without reading .proto diffs directly, the same “who’s on the other end” test internal API changelogs applies generally. A gRPC service consumed only by its own team’s other services can often skip a formal changelog in favor of the commit history, since everyone reading it already has the schema open.


The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.

Related on changeloop: Developer docs, Changelog tools compared

changeloop
The team building a closed-loop changelog. Your users ask, your team ships, the person who asked gets told.