A strong first answer: pick JSON when you need human-readable, browser-friendly, loosely-coupled contracts (a public REST API, a webhook payload, anything a developer might read in a curl response), and pick Protobuf when you control both ends of the wire (the wire = the literal bytes sent over the network, not the source code or an in-memory value) and need small, fast, strongly-typed messages (internal gRPC calls [gRPC: a common RPC framework built on Protobuf], high-throughput or high-volume traffic between your own services).
The axes that actually decide it
Payload size and latency. Protobuf is a binary, tag-length-value format: field names never travel on the wire, only small integer field numbers do, and values are packed (varints -- a compact encoding that uses fewer bytes for smaller integers -- for integers, no quoting for strings). JSON repeats every field name as a string on every message. For small, frequent messages the difference in serialization/deserialization cost and wire size adds up.
Schema evolution. Schema evolution is where the format choice matters most, because API contracts keep changing long after they first ship, and the two formats handle that change very differently. Protobuf ties every field to a stable field NUMBER, not to its position or name: a reader that does not recognize a field number simply skips it, so adding a new field is safe by construction, and renaming a field is free (the name is compile-time only, never on the wire). The one hard rule is that a field number must never be reused for a different meaning once it has shipped. JSON has no built-in schema at all; "schema evolution" for JSON is really "JSON Schema evolution," and safety depends entirely on the discipline the team layers on top (never treating a field as positionally required, always tolerating unknown fields).
Developer ergonomics and human-readability. JSON wins here outright: you can read it in a browser network tab, log it, curl it, and paste it into a bug report with zero tooling. Protobuf messages are opaque bytes without the compiled descriptor; debugging typically means adding a debug JSON-encoding path or using a tool that understands the .proto file.
Client diversity and tooling. JSON needs nothing beyond a standard library in essentially every language ever shipped, which matters when the API is public and you do not control the client. Protobuf needs the compiler and generated stubs for each client language, which is a real integration cost for external, unknown consumers but a small one-time cost inside a service mesh you already control.
Worked example: what the format choice actually costs on the wire
Take a tiny, realistic message: {"id": 42, "name": "widget", "in_stock": true}.
As compact JSON (no extra whitespace), this is:
{"id":42,"name":"widget","in_stock":true}
That is 41 bytes.
Encoding the same three fields as Protobuf (field 1 = id, varint; field 2 = name, length-delimited; field 3 = in_stock, varint), by hand-rolling the tag-length-value bytes. Every field starts with a tag byte, and that byte is not arbitrary: it packs the field number together with a wire type (a small numeric code -- 0 for varint, 2 for length-delimited -- that tells the decoder how many bytes to read and how to interpret them) using the formula tag = (field_number << 3) | wire_type. You can verify each one by hand below:
- Field 1 (varint, wire type 0): tag = (1 << 3) | 0 = 8 =
0x08; value 42 fits in one varint byte 0x2A -> 2 bytes.
- Field 2 (length-delimited, wire type 2): tag = (2 << 3) | 2 = 18 =
0x12; length byte 0x06, then the 6 raw ASCII bytes of "widget" -> 8 bytes.
- Field 3 (varint, wire type 0): tag = (3 << 3) | 0 = 24 =
0x18; value true encodes as 0x01 -> 2 bytes.
Total: 12 bytes (08 2a 12 06 77 69 64 67 65 74 18 01).
reduction=(1−4112)×100%≈70.7%
That 70.7% is specific to this exact 3-field, mostly-numeric message; it is not a universal constant. Industry write-ups on protobuf vs. JSON commonly report a broader range (roughly 50 to 85% smaller, 3 to 10 times faster to parse) across many message shapes, which is consistent in direction with this worked example but is a separately-reported range, not something this specific calculation proves on its own.
When the axes actually point in different directions
For an internal, high-throughput RPC path exchanging large model metadata or batched inputs between services you own end to end, Protobuf's size and schema-evolution guarantees dominate and the tooling cost is a one-time investment. For a public REST API serving a heterogeneous client base (mobile, web, and IoT devices you do not control), JSON's zero-tooling accessibility usually outweighs the wire-format savings, unless payloads are large enough (media, bulk exports) that the size difference becomes the bottleneck.