
Back to Blog

The Design Behind wincode, a Faster Rust Serializer
Written By
Zach Brown
TL;DR
wincode is a fast, bincode-wire-compatible serializer and deserializer for Rust, built by Anza originally for the Agave validator. It is not a fork of bincode. It is a new implementation with its own traits and derive macros that produces the same bytes as bincode's default configuration.
RustSec considers bincode unmaintained (RUSTSEC-2025-0141) and recommends wincode as a replacement. Wincode works anywhere fast serialization is needed in Rust.
Docs: docs.rs/wincode
Crate: crates.io/crates/wincode
Benchmarks
Speedups vary by hardware, element type, and payload size. A few sample benchmarks against bincode (same wire format, no hand‑rolled parsing) running locally on AMD 9950X3D:
Results
1,000 element deserialization

1,000 element serialization

Types referenced in benchmarks:
Performance comes from a unifying design philosophy: tailor the read and write paths to the shape of each type so they do the minimum work that type actually requires. The result is hand‑written‑deserializer performance from a normal, type‑driven Rust API.
Why We Built wincode
Serialization sits on the hot path of many sub-systems of the validator we care about. Over time, we kept seeing the same thing in profiles: work that was technically correct, but structurally more expensive than it needed to be.
That was the motivation for wincode. We wanted the ergonomics of type-driven, declarative serialization, compatibility with the existing bincode wire format, and performance characteristics akin to or better than what we were getting from hand-rolled deserializers.
Wire compatibility was a hard requirement, not a convenience. The validator has years of serialized state and network messages in bincode format. wincode had to read and write those bytes identically while changing everything about how the work gets done underneath.
This post explains the technical problem we were trying to solve, and the design choices that made the solution practical.
Core Problem 1: Construct, Then Move
Most Rust code naturally follows a construct-then-move pattern. You build a value, then move it into its final destination.
That is usually the right tradeoff between safety, ergonomics, and performance. But where performance is critical, operation ordering matters. If values are first materialized and only later moved into the destination allocation, extra copies become hard to avoid.
In other words, if we want to remove those copies, data needs to write directly into final memory as bytes are consumed. APIs that are shaped around "construct first, place later" make that difficult to express.
This is where the performance cliff appears: high-level code is easy to write, but the fastest path often requires lower-level control over placement and memory initialization.
Destination-First API Design
wincode addresses this by making deserialization destination-first. Instead of asking the decode path to construct a full value and return it, the API decodes into caller-provided destination memory (&mut MaybeUninit<...>).


That changes the execution order in an important way. The deserializer can allocate container storage once, then initialize values directly where they will live, rather than constructing temporary values and moving them afterward. For large or deeply nested structures, that removes a meaningful amount of redundant copying and intermediate movement.
It also makes the high-performance path explicit in the API shape. You do not have to drop down into application-level pointer choreography just to express "decode directly into final storage." The library encapsulates that low-level machinery, while application code keeps a declarative, type-driven interface.
Core Problem 2: Sequence Deserialization of Opaque Types
Generic sequence decoding has to work for all element types: variable-sized values, nested structures, custom validation, and arbitrary control flow. Because of that, the default deserialization model is element-by-element.
That model is flexible, but leaves a lot of performance on the table for fixed-size payloads. The most obvious cases are byte-like sequences, including wrappers like Vec<Signature> (where Signature is a fixed-size byte array/newtype) and Vec<u8> in paths that do not hit a specialized bytes fast path. But the same structural issue also appears with non-byte fixed-size types such as Vec<u64>.
The generic sequence path typically looks like this:
1. Decode the sequence length.
2. Allocate capacity.
3. Repeatedly deserialize each element through a generic element path.
For fixed-size types, that can mean repeated control-flow, repeated bounds checks, and repeated per-element bookkeeping over data where total byte requirements are known up front. The semantic model is generic, but the memory layout is often simple enough to plan ahead.

The path we actually want is:
1. Decode the sequence length.
2. Validate one contiguous byte window.
3. Allocate capacity.
4. Copy into final storage in one bulk operation.
For byte-like, memcpy-safe element types, step 4 can be a direct bulk copy (example below).

For fixed-size non-memcpy-able types (for example u64 under a non-native endianness configuration), we still want steps 1-3: compute total bytes as len * element_size, validate that window once, and then decode elements within that contiguous window without intermediate validation and bounds checks.


Compile-time Type Metadata
The central design idea that enables this is wincode's TypeMeta: compile-time metadata attached to types.
At a high level, TypeMeta tells wincode two things that matter for read-path optimization:
- whether a type has a statically known serialized size,
- and whether that type is raw memcpy-able and zero-copy eligible.
Because this metadata is known at compile time, wincode can select specialized execution paths without runtime type introspection.
Compile-time type metadata enables large bounds-check-elided windows
When element size is known at compile time, wincode can compute total bytes up front (for example, len * size_of::<T>() for sequences), validate a larger byte window once at the container boundary, then deserialize within that window without re-checking bounds at every intermediate step.
For nested and sequence-heavy data, this removes a large amount of repeated branching from the hot path. You still get correctness checks, but they move to fewer, more meaningful boundaries.
Compile-time type metadata enables contiguous bulk copy
For types that are fixed-width and zero-copy / memcpy eligible, wincode can use the contiguous representation directly. In container paths like Vec<T>, this means:
- one preallocation check,
- one contiguous read window check,
- one bulk copy into Vec storage.
This is the same shape we used to implement manually for fixed-byte payloads, but now exposed through wincode’s traits and derive macros.
Viewed another way, TypeMeta gives wincode three practical read modes:
1. Statically sized and memcpy-able: validate the contiguous region and bulk-copy directly into destination storage.
2. Statically sized, non-memcpy-able: validate once, then run element decode logic inside a trusted window without intermediate bounds checks (for example, structs with fixed sized members without a guaranteed layout).
3. Dynamic: fall back to the fully general path when static sizing is not available.
That split is a key reason wincode can stay general-purpose at the API level while still producing highly specialized behavior.
Compile-time type metadata enables seamless zero-copy
Because wincode carries complete metadata for the types it reads, it can determine when serialized bytes and in-memory layout line up exactly such that it can form references directly from the payload.
The derive macros generate this metadata automatically – they can deterministically infer when zero-copy deserialization is valid and when it is not, based on the same representation constraints the library enforces for safety.
That is what makes zero-copy both usable and predictable in practice. You do not hand-write unsafe reference-casting logic at each call site. The type metadata drives the decision, and the library only allows forming references when the type/configuration combination satisfies zero-copy requirements.
What This Changes for Application Code
The main outcome is that most application code can stay in safe Rust and still hit or exceed performance targets that previously pushed us toward hand-rolled deserializers.
The unsafe machinery does not vanish, but it becomes centralized in one place: the library implementation and its contracts. That makes it auditable, testable, and fuzzable as a shared piece of infrastructure, rather than a pattern every team has to reimplement.
Hand-rolled Deserializers Are Not a Good Steady State
Before wincode, when we needed this performance profile, we often wrote manual deserialization implementations: direct byte parsing, explicit placement writes, carefully ordered initialization, and cleanup logic for partial failures.
That can absolutely be fast, but it is difficult to maintain.
As types and APIs evolve, manual parsing code becomes expensive to review and easy to regress. Safety requirements are spread across call sites. Subtle bugs hide in edge cases around initialization and error unwinding. The engineering cost grows with every new type and every version transition.
We wanted to keep or exceed the performance characteristics of hand-rolled deserializers, but move that complexity out of application code.
Why This Matters
wincode is not about replacing existing tooling for its own sake. It is about closing a real gap between ergonomic serialization code and the performance profile required in high-performance systems.
The goal is straightforward: deliver hand-rolled-deserializer class performance, without inheriting hand-rolled-deserializer maintenance burden everywhere else.
FAQ
Is wincode a fork of bincode?
No. wincode is a separate implementation that shares no code with bincode. It has its own configuration system for options like endianness and fixed vs. varint integer encoding. wincode's default configuration matches bincode 1.x's default configuration byte for byte, and equivalent custom configurations on each side produce the same wire format.
Is wincode a drop-in replacement for bincode?
Yes. As long as your type layout and configuration match, wincode reads and writes the same bytes bincode does. Swap the derive macros to SchemaWrite and SchemaRead and you are done.
Does wincode depend on serde?
No. wincode has its own traits and derive macros. serde-based types and wincode-based types can coexist in the same codebase and exchange bytes.
Is wincode only for Solana?
No. Nothing in wincode is Solana-specific. It is Apache-2.0, supports no_std, and integrates with bytes, smallvec, indexmap, uuid, and bumpalo.
Try wincode
Add wincode to your Cargo.toml and try it yourself: github.com/anza-xyz/wincode