Engineering Perspective
Who this page is for
- Backend and platform engineers choosing API and RPC
payloads - Performance-minded developers caring about CPU, allocations, and latency tails
- Anyone who must deserialize untrusted input
- Engineers aligning local choices with a multi-language estate
Four families (suite-aligned)
The benchmark suite groups serializers into paradigms. Compare within a paradigm and within one language before crowning a global winner.
| Family | Examples | Schema on wire | Human-readable | Typical home |
|---|---|---|---|---|
| Text / JSON |
JSON, sometimes XML |
Optional / external | Yes | Public APIs, config, debug-friendly logs |
| Schemaless binary | MessagePack |
Type tags / field names often present | No | Internal services, caches, queues |
| Schema-driven | Protobuf |
Numbers/layout from schema | No | Stable contracts, high-throughput RPC/streams |
| Language-native | pickle |
Runtime type metadata | No | Same-stack caches and graphs (trust carefully) |
Decision sketch (services)
- Need humans to read/edit the payload on the wire?
- Yes → JSON family (add JSON Schema / OpenAPI when contracts matter)
- No → continue below
- Need shared IDL/schema and multi-language evolution rules?
- Yes → Schema-driven (Protobuf-like, Avro-like, or zero-copy IDL)
- No → continue below
- Single language/runtime, complex graphs, fully trusted data?
- Yes → Language-native only inside a hard trust boundary
- No → Schemaless binary (MessagePack / CBOR / …) and validation at edges
Text-based interchange
JSON is the default public contract: universal parsers, easy logging, mediocre density and parse cost. Gaps (dates, binary, int vs float) are managed by convention or by a validation layer (JSON Schema
, OpenAPI
, typed request models).
XML remains in enterprise and document systems; prefer it when the ecosystem already demands it, not as a greenfield API default.
YAML / TOML
are configuration formats more than wire formats. YAML’s complexity has a long security history with “load untrusted YAML” mistakes—prefer safe loaders and locked-down schemas for untrusted input.
Schemaless binary
MessagePack, CBOR, and BSON keep a dynamic data model while dropping text parsing. Field names or type tags usually still appear, so they are typically larger than a tight Protobuf encoding but smaller/faster than JSON.
Engineering uses: internal HTTP
/RPC bodies, Redis
-style values, multi-language without an IDL mandate.
You still own: validation, compatibility, and documentation.
Schema-driven binary
Protocol Buffers — field numbers, codegen, strong multi-language story, explicit evolution discipline (don’t reuse field numbers; reserve deleted ids).
Apache Thrift
— IDL + pluggable protocols/transports; historically RPC-centric polyglot stacks.
Apache Avro — often chosen when schema resolution and data-platform interoperability matter (also covered under data science); appears in event pipelines as much as in classical RPC.
FlatBuffers / Cap’n Proto
— design for low-parse / zero-copy access; excellent read paths; different mutation and tooling ergonomics than classic “build a struct → serialize” Protobuf style.
Language-native
Convenient for object graphs inside one runtime. Treat as unsafe by default on the network or any multi-tenant input path. Prefer portable formats whenever data leaves the process trust domain.
Performance mechanics
Numbers belong on Results pages. These are the mechanisms those numbers come from.
Data locality and CPU caches
Modern CPUs are fast; random memory access is not. Serializers that scatter fields via pointer-rich object graphs cause cache misses. Designs that keep related bytes contiguous (and zero-copy formats that read from a single buffer) reduce stalls.
When you benchmark, payload shape matters as much as codec brand: deep pointer graphs punish every language; dense structs favor contiguous layouts.
Allocations and garbage collection
In managed runtimes (C#, Java, Python, JS, Go), allocation rate drives GC work and latency spikes.
| Pattern | Effect |
|---|---|
| Allocate a new string/array per field | High GC pressure under load |
| Decode into reused buffers / pooling | Lower allocator traffic |
Span-like views over existing memory |
Avoid copies when APIs allow |
| Zero-copy formats | “Deserialize” may mean bounds-checked views, not new objects |
“Faster serializer” often means fewer allocations, not only fewer CPU instructions in the encode loop.
Zero-copy deserialization
Traditional path: bytes → parse → new language objects (copy).
Zero-copy path (FlatBuffers, Cap’n Proto, and some buffer-oriented APIs): wire layout is arranged so fields are readable in place. Trade-offs include validation discipline (skipping a parse can skip structural checks if you are careless), less friendly partial mutation, and operational tooling differences.
Text parsing cost
JSON/XML must discover tokens, unescape strings, and convert decimal text to binary numbers. Binary formats largely avoid that. At scale this is both CPU and energy/cost in the datacenter—not just academic microbenchmarks.
Size vs speed
Smaller payloads help networks and storage; the fastest codec is not always the smallest. Measure your payloads (see suite topologies) rather than blog leaderboards alone.
Security: deserialization
Untrusted bytes are hostile input.
| Risk | Where it shows up | Mitigation |
|---|---|---|
| Remote code execution via native deserialize | Java serialization, pickle, some legacy binary formatters, careless YAML load |
Never deserialize untrusted native formats; prefer pure data formats + explicit allowlists |
| Billion laughs |
XML | Disable external entities; use safe parser settings |
| Resource exhaustion | Huge nested JSON, deeply nested CBOR/msgpack, unbounded collections | Limits on depth, size, and allocations |
| Logic bugs from type confusion | Schemaless JSON (“number or string?”) | Validate with a schema or typed model at the trust boundary |
| Skipping verification in zero-copy | FlatBuffers-style buffers used without a verifier | Always verify untrusted buffers before use |
Rule of thumb: the more powerful the deserializer (arbitrary types, dynamic code), the smaller the set of inputs it may see.
Schema evolution for services
Services rarely deploy atomically. Plan for old readers + new writers and the reverse.
| Approach | Practical guidance |
|---|---|
| Protobuf field numbers | Add optional fields; never repurpose numbers; reserved deleted ids |
| JSON + consumers | Additive changes are safer; renames break silently; use API versioning when removing fields |
| Avro compatibility modes | Encode policy in a registry/CI (backward/forward/full) |
| “We’ll fix it in the client” | Does not scale past one team |
Document whether fields are required, defaulted, or nullable—wire format cannot invent product semantics.
Operational concerns
- Debuggability: JSON in logs vs binary needing decoders and schema versions in observability tooling
- Gateways and mesh: some exotic RPC framings interact poorly with vanilla HTTP/2
load balancers and serverless edges - Codegen in CI: schema-driven stacks need stable
protoc/IDL pipelines and versioned generated artifacts - Polyglot drift: “we use Protobuf” is incomplete without a shared style guide (well-known types, error model, timestamp policy)
- Partial failure: corrupts and truncated frames need clear errors, not hung parsers
Worked choice patterns
| Scenario | Reasonable default | Why |
|---|---|---|
| Public HTTP API for third parties | JSON + OpenAPI | Ecosystem and debuggability dominate |
| Internal microservice RPC, multi-language | Protobuf (or similar) over your standard transport | Compact, typed, evolvable |
| Hot cache of dynamic documents | MessagePack/CBOR or JSON depending on clients | Schemaless binary if all consumers agree |
| Same-process or same-runtime trusted cache | Language-native only if threat model allows | Otherwise portable binary |
| Ultra-low-latency read of large immutable messages | FlatBuffers / Cap’n Proto-class design | In-place access |
| Analytics export from a service | Write Parquet |
Do not force OLTP message formats to be your lake |
Illustrative snippets
Orientation only—not library endorsements. (Site-wide fenced code uses plain highlighting; see mkdocs.yml.)
JSON (public API style)
import json
payload = {"name": "Alice", "scores": [95, 87]}
text = json.dumps(payload, separators=(",", ":"), sort_keys=True)
obj = json.loads(text)
MessagePack (schemaless binary)
import msgpack
packed = msgpack.packb({"nums": [1, 2, 3]})
assert msgpack.unpackb(packed) == {"nums": [1, 2, 3]}
Protobuf-style (after codegen)
# Generated module provides message classes (illustrative Google tutorial names).
person = addressbook_pb2.Person(id=1234, name="Alice")
data = person.SerializeToString()
person2 = addressbook_pb2.Person()
person2.ParseFromString(data)
Key takeaways
- Pick a paradigm first, then a library—suite categories exist to prevent unfair cross-paradigm comparisons.
- Public edge ≠internal hot path—JSON at the boundary and binary inside is a normal, historical pattern.
- Performance is layout + allocations + parsing, not a single brand name.
- Untrusted deserialize is a security boundary—native serializers are not “just faster JSON.”
- Evolution is a process (IDs, registries, API versions), not only a file format.
- Measure on your payloads with this suite’s topologies and your language’s Results.
References
- RFC 8259 (JSON); JSON Schema and OpenAPI documentation
- MessagePack specification; CBOR RFC 8949
- Protocol Buffers language guide and style guides
- Apache Thrift and Apache Avro project docs
- Cap’n Proto and FlatBuffers documentation (encoding + security/verification notes)
- Language security docs for pickle / Java serialization / legacy binary formatters