πŸ—‚οΈ Formats Compared

Five formats, four axes: is there a schema, is it binary, does the schema ride with the data, and can you read a field without parsing the whole thing? Answer those and the right pick falls out.

The Master Table

FormatSchemaBinarySelf-describingZero-copySweet spot
JSONNone❌ Textβœ… Names inline❌Public APIs, config, logs
ProtobufRequired (.proto)βœ…Tags only❌Internal RPC, gRPC
AvroRequired (.avsc)βœ…Schema stored with data❌Data lakes, Kafka, batch
MessagePackNoneβœ…βœ… Names inline❌"binary JSON" drop-in
FlatBuffersRequired (.fbs)βœ…Tags onlyβœ… Read in placeGames, mmap, latency-critical

The Two Axes That Decide It

flowchart TB Q1{"Need humans to read it
or unknown consumers?"} Q1 -->|Yes| JSON["JSON"] Q1 -->|No| Q2{"Read fields without
parsing the whole msg?"} Q2 -->|Yes| FB["FlatBuffers"] Q2 -->|No| Q3{"Schema travels
with the data?"} Q3 -->|"Yes (storage/streams)"| AVRO["Avro"] Q3 -->|"No (RPC, tags on wire)"| PB["Protobuf"]

Protobuf vs Avro β€” the subtle one

Both are schema-ful and binary, but they differ in where the schema lives:

Rule of thumb: Protobuf is built for RPC, where messages are self-contained and each side compiled the schema. Avro is built for storage & streaming, where millions of records share one schema that's cheaper to store once than to tag on every row.

MessagePack β€” binary JSON

MessagePack is JSON's data model (maps, arrays, strings, numbers, bools, null) packed into bytes. No schema, still self-describing β€” field names travel with the data β€” but numbers and structure are binary, so it's smaller and faster to parse than text JSON.

Use it as a drop-in when you want JSON's flexibility and dynamic shape but a lighter wire β€” caches, Redis values, mobile sync. You don't get Protobuf's tiny numeric tags or its enforced schema evolution, because the names still ride along on every message.

FlatBuffers β€” the zero-copy trick

Protobuf, Avro, and MessagePack all deserialize: they walk the bytes and build an object graph before you can touch a field. FlatBuffers lays out data so that a field's location is computable from offset tables β€” you read obj.hp() directly out of the received buffer with no parse step and no allocation.

flowchart LR subgraph Std["Protobuf / Avro"] B1["bytes"] -->|"full parse + alloc"| O1["object graph"] --> R1["read field"] end subgraph FBs["FlatBuffers"] B2["bytes (mmap'd)"] -->|"offset lookup"| R2["read field in place"] end
The cost: the format is bulkier on disk (alignment padding, vtables), building messages is more awkward, and the win only shows up when you read a few fields from large buffers very often. For "parse the whole message anyway" workloads, Protobuf is simpler and usually smaller.

Pick in One Sentence

If you...Use
expose a public / browser-facing APIJSON
run internal RPC between your own servicesProtobuf (via gRPC)
stream/store billions of records with evolving schemasAvro
want smaller JSON without adopting a schemaMessagePack
need to read a few fields from big buffers with zero parse costFlatBuffers
Real-world: most systems mix them by layer β€” JSON at the public edge, Protobuf between services, Avro in the data pipeline behind. There's no single winner; there's a right tool per boundary. To see Protobuf carry real traffic, head into the gRPC collection.