#1302
refactor: replace unmaintained bincode with postcard + borsh
Summary
Replace the unmaintained bincode crate (RUSTSEC-2025-0141) with two actively maintained serialization backends behind feature gates.
Changes
Dependencies
- Removed:
bincode = "1"(workspace) - Added:
postcard = "1"(serde-native, community standard replacement) - Added:
borsh = { version = "1", features = ["derive"] }(NEAR Protocol, high performance)
Feature Gates
| Features | Active blanket Code impl |
|---|---|
| (none) | Manual impls for numeric types, bool, Vec<u8>, String, Bytes |
serde only | impl Code for T: Serialize + DeserializeOwned via postcard |
borsh only | impl Code for T: BorshSerialize + BorshDeserialize via borsh |
serde + borsh | borsh takes priority (avoids trait coherence conflicts) |
Modified Files
Cargo.toml— workspace depsfoyer-common/Cargo.toml— features and dependenciesfoyer-common/src/code.rs— three mutually exclusiveCodeimpl paths with#[cfg]foyer-common/src/error.rs—bincode_error()->postcard_error()foyer/Cargo.toml— addborshpass-through featureexamples/Cargo.toml— addborshpass-through feature
New Benchmark
foyer-common/benches/bench_serde/ rewritten to directly compare postcard, borsh, and manual encoding:
- postcard.rs — postcard backend (
#[cfg(feature = "serde")]) - borsh.rs — borsh backend (
#[cfg(feature = "borsh")]) - manual.rs — hand-written LE encoding (always available)
Benchmark Results
Test machine: Linux 6.8, 100 samples per benchmark, 3s warmup.
Test data: struct Entry { id: u64, label: String, payload: Vec<u8> }
Encode
| Size | manual | borsh | postcard |
|---|---|---|---|
| 64 KiB | 539 ns | 1.04 us (1.9x) | 16.8 us (31x) |
| 4 MiB | 51.9 us | 101 us (1.9x) | 1.11 ms (21x) |
| 64 MiB | 3.83 ms | 19.3 ms (5.0x) | 20.6 ms (5.4x) |
Decode
| Size | manual | borsh | postcard |
|---|---|---|---|
| 64 KiB | 541 ns | 866 ns (1.6x) | 27.0 us (50x) |
| 4 MiB | 50.7 us | 76.9 us (1.5x) | 1.74 ms (34x) |
| 64 MiB | 12.3 ms | 14.6 ms (1.2x) | 38.0 ms (3.1x) |
Encoded Size (raw = id + label + payload)
| Size | raw | manual | borsh | postcard |
|---|---|---|---|---|
| 64 KiB | 65552 B | 65568 B (+16) | 65560 B (+8) | 65557 B (+5) |
| 4 MiB | 4194320 B | 4194336 B (+16) | 4194328 B (+8) | 4194326 B (+6) |
| 64 MiB | 67108880 B | 67108896 B (+16) | 67108888 B (+8) | 67108887 B (+7) |
Key Findings
- borsh stays close to manual — only 1.5-2x slower than raw
memcpyfor both encode and decode across all sizes - postcard is significantly slower for large payloads: 21x slower encode and 34x slower decode than manual at 4 MiB. The varint encoding overhead dominates for bulk data.
- At 64 MiB the gap narrows as memory bandwidth becomes the bottleneck — borsh ~5x, postcard ~5.4x vs manual for encode
- All three produce nearly identical encoded sizes — only 5-16 bytes of metadata overhead regardless of payload size
- manual encoding is always fastest but requires handwritten encode/decode — the baseline cost is essentially
memcpy
Migration Notes
- Wire format change: postcard and bincode produce different byte representations. Disk caches written with bincode cannot be read with postcard. Users with persistent disk caches must clear or migrate them.
Error::bincode_error()has been replaced withError::postcard_error()(available when theserdefeature is active andborshis not).- Users who derived
Serialize + DeserializeOwnedforCodecontinue to work with theserdefeature, no code changes needed. - Users who want maximum performance should consider the
borshfeature and deriveBorshSerialize + BorshDeserialize.
Generated with Claude Code
Codecov Report
:x: Patch coverage is
10.00000%with18 linesin your changes missing coverage. Please review.75.26% <0.00%> (+1.16%)80.82% <12.50%> (-5.31%)... and 2 files with indirect coverage changes
:rocket: New features to boost your workflow: