Photo by Mika Baumeister on Unsplash
Binary Serialization Formats: Size vs Speed Tradeoffs
When systems need to move data between processes, across networks, or into storage, the serialization format determines more than just wire size. Binary formats optimize for different points on the spectrum between compactness, parsing speed, and memory efficiency. Understanding these tradeoffs helps explain why different systems reach for different tools.
Variable-Length Encoding and Compression
Protocol Buffers and similar formats use variable-length integer encoding (varints) to represent numbers compactly. Small integers take fewer bytes: the number 127 fits in one byte, while larger values expand to multiple bytes. This works well for workloads with many small numbers but creates a problem: you can’t jump directly to the fifth field without parsing the first four.
The size wins compound with optional fields. Unlike fixed-width formats that reserve space for every possible field, protobuf omits fields set to their default values entirely. A message definition with fifty fields might serialize to just a few bytes if most fields are unset. This sparse encoding saves bandwidth but requires the parser to reconstruct the full object representation, checking which fields were present and filling in defaults.
Some formats like Cap’n Proto take the opposite approach: fixed-width encoding with mandatory padding. Every field occupies predetermined space, even if empty. The result is larger messages but zero-copy access—the serialized bytes can be read directly as in-memory structures without parsing.
Parse-Then-Use vs Direct Access
Traditional serialization requires two phases: deserialize the bytes into language-native objects, then access the data. This adds latency and allocates memory. For a JSON API response, you parse the entire payload before reading the single field you need.
FlatBuffers and Cap’n Proto eliminate this phase by designing the binary layout to match memory access patterns. Fields are stored at known offsets with direct pointers. To read a field, you follow an offset table—no deserialization pass required. This works especially well for large messages where you only need a subset of fields, like reading one column from a database result set.
The tradeoff is write complexity. Building a FlatBuffers message requires pre-calculating sizes and writing data in reverse order (depth-first, backwards) because offsets must be known before writing parent structures. Protocol Buffers, by contrast, can stream data as it’s generated.
Schema Evolution and Versioning
Binary formats handle schema changes differently. Protocol Buffers assign each field a unique number. Adding new fields doesn’t break old readers—they skip unknown field numbers. Removing fields is safe as long as you never reuse the number. This forward and backward compatibility comes from self-describing messages: each field is tagged with its number and wire type.
Formats optimized for zero-copy access give up some evolution flexibility. FlatBuffers requires readers and writers to share the schema definition. Adding fields works (new readers can handle old data), but the schema file itself becomes part of the contract. There’s no embedded metadata describing what fields are present.
Thrift and Cap’n Proto take a middle path with explicit versioning markers, allowing readers to detect and handle schema mismatches rather than silently misinterpreting bytes.
Alignment and Memory Access
Modern CPUs penalize unaligned memory access—reading a 4-byte integer that doesn’t start on a 4-byte boundary is slower, and on some architectures it causes a fault. Text formats like JSON have no alignment since everything is parsed into fresh allocations. Fixed-width binary formats must pad structures to maintain alignment, trading size for speed.
FlatBuffers enforces strict alignment, padding fields so integers land on natural boundaries. A struct with three bytes of data might serialize to eight bytes after padding. This overhead is the cost of direct memory mapping—the bytes on disk must be valid in-process memory layouts.
Variable-length formats like MessagePack pack bytes tightly without padding, accepting the CPU penalty for dense storage. The parser handles unaligned reads during deserialization, centralizing the performance cost.
Picking the Right Format
Protocol Buffers fits most RPC systems: balanced size, good parsing speed, strong schema evolution. MessagePack works well for dynamic data or scripting languages where schema flexibility matters more than peak performance. FlatBuffers excels in game engines and embedded systems where parse time dominates and memory access patterns are predictable.
The common thread is specialization. Binary formats don’t obsolete JSON—they shift costs around. Smaller messages mean higher CPU usage during encoding. Faster parsing means larger payloads or restricted schema changes. Understanding where your bottleneck lives determines which tradeoff makes sense.