Generate a follow-up sub-lesson on any aspect of this topic
Guide complete
Choosing JSON vs Protobuf vs Avro
Generate a follow-up sub-lesson on any aspect of this topic
Choosing JSON vs Protobuf vs Avro
TLDR;
- The same
OrderCreatedcan cost meaningfully different network and storage at RPS and /s events.- Public APIs usually pay for debuggability, while internal RPC paths pay for CPU and tail latency.
- Schema evolution fails differently, and the failure mode often decides the format.
A checkout system hits three different boundaries where the bytes matter. The public web API has browser and mobile clients that need to understand failures and payloads quickly. The internal call from Checkout to Order runs at higher QPS and cares about p95 latency and server CPU. The event stream out to Kafka and into object storage is a long lived record that downstream jobs will replay and join months later. A single serialization choice rarely fits all three boundaries, so the decision starts by naming the boundary and the dominant constraint on it.
We will keep one running message, OrderCreated, and watch how JSON, Protobuf, and Avro behave when that message crosses each boundary. The interesting differences are not philosophical. They show up as framing overhead on the wire, parse costs inside hot services, and the rules each format uses when fields appear, disappear, or change types over time. First, anchor the system we are moving bytes through.
Serialization exists because services and disks store bytes, not objects, and every boundary needs a way to delimit messages. Framing is the rule that tells a reader where one message ends and the next begins, such as a length prefix on a TCP stream or a record container in a file. When payloads are verbose, the cost is not just bandwidth. The load balancer pushes more bytes, the client spends longer on slow links, and the service burns more CPU copying and parsing text. Under steady load, those small per message differences turn into capacity decisions.
For a concrete intuition, treat total traffic as rate times average size. If the public API receives requests per second and each response body averages bytes, then egress is roughly bytes per second before TLS and HTTP overhead. The same multiplication drives daily object storage for events. That is why a format that is only a few hundred bytes bigger per message can still force extra partitions in Kafka or earlier log compaction decisions. Let’s plug the running checkout numbers into that multiplication.
The simplest way to compare formats is to hold the logical message constant and change only the encoding. Here is OrderCreated with a handful of fields such as order_id, user_id, total_cents, currency, and an array of line items. In JSON, the schema is implicit in the field names and the producer just emits text. In Protobuf, a .proto schema assigns each field a stable number and the encoded message omits field names. In Avro, the record is written against a schema that can travel with the data file or be fetched by id from a registry, and readers resolve writer and reader schemas at consumption time.
The observable differences line up along a few axes. JSON is readable in logs and curl output but tends to be larger and slower to parse due to text and repeated field names. Protobuf and Avro are binary so they usually reduce size and parse faster, but they shift work into schema governance and tooling. Protobuf is optimized for RPC style messages with generated code. Avro is optimized for datasets and event logs where schema resolution and long term compatibility are first class. Now map the same payload across the three encodings.
Readability matters most where humans debug with partial context. A public HTTP API often fails in ways a client developer needs to inspect quickly. With JSON, a developer can paste a response into a ticket, diff two payloads, and spot an unexpected null without any extra tooling. That operability is a real feature when the API surface is wide and client diversity is high, because it reduces the time to isolate which side violated an assumption.
Binary formats can still be operable, but the system needs more scaffolding. Protobuf needs the right .proto at the right version to decode a blob captured in logs, and without it most observability tools show opaque bytes. Avro in pipelines commonly relies on a schema registry and disciplined subject naming so a consumer can fetch the writer schema. When teams skip that governance, the failure mode shifts from messy payloads to unreadable ones, and debugging becomes a schema archaeology exercise rather than a request trace.
Design rule
Pick the most readable format at the boundary with the most unknown clients.
Compactness dominates when the same payload is sent or stored many times. The public API path pays for egress, client download time, and CDN cache fill. The event path pays for Kafka throughput, retention, and object storage footprint. In those places, smaller messages raise the ceiling before the system needs more partitions, more brokers, or more aggressive retention. Binary formats often win here because they avoid repeating field names and represent numbers as varints or fixed width bytes instead of decimal text.
CPU dominates when QPS is high and services are latency sensitive. Internal RPC calls like Checkout to Order tend to be frequent, and the server does encode, decode, and validation on every call. Text parsing can become visible in profiles as allocations and UTF 8 decoding, while generated Protobuf code can decode directly into typed fields with fewer intermediate objects. The win condition is not simply smaller. It is fewer CPU cycles per request at p95 when the service is near saturation. We can tune that tradeoff across the three boundaries in the running system.
Schema evolution is the long term cost that shows up after the first few deploys. Schema evolution is the set of rules that let an old reader consume new data, and a new reader consume old data, without coordinated deploys. JSON often uses schema by convention. Producers add a field, consumers ignore unknown fields, and breaking changes are avoided socially or via separate versioned endpoints. That works until a field changes meaning or type and different clients interpret the same bytes differently.
Protobuf and Avro encode evolution rules into the system. Protobuf depends on stable field numbers and treats unknown fields as skippable, so adding an optional field is usually safe while reusing a field number for a different meaning is a long lived footgun. Avro uses writer and reader schemas, and at read time it resolves differences using field names and defaults. That makes some migrations, like adding a field with a default, straightforward in an event log where old and new records coexist. The flip side is that schema management becomes part of availability. If the schema registry is down, consumers can stall even if Kafka is healthy. The safe and unsafe moves look different across the three formats.
A good decision matrix uses consistent criteria and ties them to a boundary. For the running checkout system, the criteria that usually decide are client diversity, debuggability, bandwidth and storage pressure, latency and CPU pressure, and how strict schema governance needs to be. The same team can reasonably pick different formats for different boundaries because the operational constraints differ.
The matrix is not about scoring a winner. It is about naming the constraint that dominates at each edge. Public HTTP tends to reward debuggability and loose coupling. Internal gRPC tends to reward generated stubs and predictable performance. Kafka and data lake paths tend to reward explicit schemas and long term replay with compatible readers. With those criteria in view, compare JSON, Protobuf, and Avro side by side.
For the running checkout system, the default set of choices is defensible. JSON fits the public API because humans and heterogeneous clients benefit from readability and flexible payloads, and versioning can be handled at the endpoint contract. Protobuf fits the internal Checkout to Order RPC because generated types and compact binary encoding reduce CPU and tail latency at high QPS. Avro fits the Kafka to object storage pipeline because explicit schemas and reader writer resolution match long lived event logs and analytics jobs that will evolve independently of producers.
Each pick should come with a named constraint that would force a redesign. If browser native clients require zero build step and direct inspection, JSON stays. If regulatory or partner integrations demand strict compatibility guarantees and formal change control, Protobuf or a rigorously validated JSON schema process becomes more attractive on the public boundary. If the event log must support replay and backfills over years with many independent consumers, Avro with strong schema governance stays. If the pipeline shifts to mostly short lived internal events with no long term storage, Protobuf on Kafka may become simpler. See how those constraint flips change the choice.