JSON's biggest weakness is also its biggest strength
JSON is text — human-readable, universally supported, trivial to debug by opening a file. That same textual nature is exactly why it's inefficient at scale: every number is spelled out as digits instead of stored as raw bytes, every key name is repeated in full on every single record, and there's no way to distinguish an integer from a float without re-parsing the string. For a config file or a low-volume API, none of this matters. For a high-throughput message queue or a data lake with billions of records, it adds up to real latency and storage cost — which is exactly the gap MessagePack, CBOR, and Avro each fill, in different ways.
MessagePack: "JSON, but binary"
MessagePack is the most direct binary analog to JSON — same data model (objects, arrays, strings, numbers, booleans, null), just encoded as compact bytes instead of text, with no schema required:
{ "name": "Ada", "age": 36, "active": true }JSON (text): 43 bytes: {"name":"Ada","age":36,"active":true}
MessagePack (hex): ~24 bytes: 83 a4 6e 61 6d 65 a3 41 64 61 a3 61 67 65 18 a6 61 63 74 69 76 65 c3The savings come from small, mechanical wins: a string's length is stored as a byte prefix rather than needing a closing quote to scan for, small integers pack into a single byte instead of ASCII digits, and there's zero whitespace. Because it's schemaless like JSON, it's a near drop-in replacement wherever you already pass around dynamically-shaped objects:
// npm install @msgpack/msgpack
import { encode, decode } from "@msgpack/msgpack";
const packed = encode({ name: "Ada", age: 36, active: true });
const obj = decode(packed); // { name: "Ada", age: 36, active: true }Redis, RabbitMQ, and several RPC frameworks support MessagePack natively as a faster wire format for exactly this reason — it needs no schema registry or code generation step, just a library on each end.
CBOR: the IETF standard, built for constrained devices
CBOR (Concise Binary Object Representation, RFC 8949) solves largely the same problem as MessagePack but as a formal IETF internet standard, with particular attention to extremely resource-constrained environments — IoT sensors, embedded devices, low-power network protocols:
// npm install cbor
import { encode, decode } from "cbor";
const packed = encode({ temperature: 22.5, sensorId: "s-042" });
const obj = decode(packed);CBOR is the encoding underneath WebAuthn (the passkey/security-key standard) and COSE (CBOR Object Signing and Encryption, used in some verifiable-credential systems) — if you've implemented passwordless login, you've already shipped CBOR without necessarily noticing, since browser WebAuthn APIs handle the encoding for you.
Avro: schema-first, built for big data pipelines
Avro takes a fundamentally different approach: instead of being schemaless like JSON/MessagePack/CBOR, every Avro file or message is paired with an explicit schema, and the encoded bytes contain almost no field names at all — just the values, in the exact order the schema defines:
// The schema (defines field names, types and order — stored once)
{
"type": "record",
"name": "User",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "age", "type": "int" },
{ "name": "active", "type": "boolean" }
]
}Because the schema already declares that field 1 is a string, field 2 an int, and field 3 a boolean, the encoded message for {"name": "Ada", "age": 36, "active": true} never needs to repeat the strings "name", "age", or "active" at all — dramatically smaller than JSON or even MessagePack/CBOR once you have many records sharing one schema, which is exactly the case in a Kafka topic or a data warehouse table with millions of rows.
Avro also has a defining feature the other formats don't: schema evolution rules — you can add a field with a default value, or remove one, and old and new schema versions remain compatible, which is why Avro (alongside Protobuf) dominates as the serialization format inside Kafka-based data pipelines and data lakes.
Side-by-side comparison
| Format | Schema required? | Typical size vs JSON | Best for |
|---|---|---|---|
| JSON | No | Baseline | APIs, config, human-readable debugging |
| MessagePack | No | ~40–60% smaller | Drop-in JSON replacement, caches, RPC |
| CBOR | No | ~40–60% smaller | IoT, embedded, WebAuthn/COSE |
| Avro | Yes | ~70–90% smaller (many records, shared schema) | Kafka topics, data lakes, big data pipelines |
| Protobuf* | Yes | ~70–90% smaller | gRPC, cross-language typed APIs |
\*See Protobuf vs JSON: When to Switch for a deeper comparison specifically against Protobuf, which shares Avro's schema-first philosophy but targets RPC APIs rather than data pipelines.
Choosing between them
The decision mostly comes down to one question: do you already have (or want) a shared schema? If your data is genuinely dynamic and schemaless — arbitrary JSON blobs, user-defined structures — MessagePack or CBOR are easy wins with almost no design cost. If you're building a data pipeline where every record shares a known, evolving structure (a Kafka topic, a data warehouse table), Avro's schema-driven compactness and built-in evolution rules are worth the upfront schema design.
Schemaless, need speed/size over JSON → MessagePack (general) or CBOR (IoT/standards-compliant)
Schema-first, high-volume pipeline → Avro (or Protobuf for RPC specifically)
Human-readable, low volume, debugging → Plain JSON — don't over-engineer thisDebugging binary formats (the part JSON makes trivial)
The single biggest cost of any binary format is that you can no longer just open the file and read it — every debugging step needs a decode step first. A practical workflow: decode the binary payload back to JSON at the point where you need to inspect it, then use ordinary JSON tooling from there:
import { decode } from "@msgpack/msgpack";
console.log(JSON.stringify(decode(packedBuffer), null, 2));Paste that decoded JSON into JSONKit's JSON Formatter to read a large or deeply nested payload, or the JSON Validator to confirm a decoded structure matches what you expect before debugging further upstream.