Skip to content

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 family JSON, sometimes XML/YAML at edges Optional / external Yes Public APIs, config, debug-friendly logs
Schemaless binary MessagePack, CBOR, BSON, many “binary JSON” codecs Type tags / field names often present No Internal services, caches, queues
Schema-driven Protobuf, Avro, FlatBuffers, Bond, many IDL tools Numbers/layout from schema No Stable contracts, high-throughput RPC/streams
Language-native pickle, Java serialization, legacy .NET binary formatters Runtime type metadata No Same-stack caches and graphs (trust carefully)

Decision sketch (services)

  1. Need humans to read/edit the payload on the wire?
  2. Yes → JSON family (add JSON Schema / OpenAPI when contracts matter)
  3. No → continue below
  4. Need shared IDL/schema and multi-language evolution rules?
  5. Yes → Schema-driven (Protobuf-like, Avro-like, or zero-copy IDL)
  6. No → continue below
  7. Single language/runtime, complex graphs, fully trusted data?
  8. Yes → Language-native only inside a hard trust boundary
  9. 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 / entity expansion 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 (or ship to a pipeline that does)—see data science 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

  1. Pick a paradigm first, then a library—suite categories exist to prevent unfair cross-paradigm comparisons.
  2. Public edge ≠ internal hot path—JSON at the boundary and binary inside is a normal, historical pattern.
  3. Performance is layout + allocations + parsing, not a single brand name.
  4. Untrusted deserialize is a security boundary—native serializers are not “just faster JSON.”
  5. Evolution is a process (IDs, registries, API versions), not only a file format.
  6. 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