Leaving bincode
bincode 1.3.3 is unmaintained, permanently. The crate that serialized our CRDT sync blobs had no future, so we moved it to postcard. The diff was ten lines. The thinking was not.
I deleted a serialization crate this week and the sync blobs got four times smaller by accident. That sentence is unfair to the crate, which did its job fine. It is fair to the process that replaced it, because the smaller blobs were never the plan.
The crate was bincode 1.3.3. The RustSec advisory is RUSTSEC-2025-0141, and its wording is unusually blunt: development has ceased permanently, and there is no safe upgrade within the 1.x line. Not "a patch is coming." Ceased. The advisory names four ways out: postcard, bitcode, wincode, rkyv.
What the crate was doing
In MossyMesh's consensus crate, two functions in crdt.rs carry every CRDT delta between islands: Delta::encode and Delta::decode. A delta is the list of operations one disconnected island needs so it can converge with another: inserts, deletes, map writes. encode turned that list into bytes; decode turned bytes back into a list. Both called bincode. When two islands sync, these are the bytes on the wire.
My first question with any advisory is whether the crate sits on the hot path or just in the tree. bincode sat exactly on the hot path. Every island sync flows through those two functions. That settled the priority. The advisory itself carries no CVE score and no published exploit. An unmaintained crate fails by never failing, until the day it does. We had already decided, the week before with serde_cbor, that "works fine today" is not a maintenance strategy.
Choosing postcard
The advisory names four successors, so the choice needed a reason beyond taste. I picked postcard for the most boring reason available: it speaks serde, the same derive-based API bincode used, so the call sites map one to one. It is no-std friendly, actively maintained, and it is the choice other projects keep landing on. The rand crate swapped bincode for postcard as the encoding in its serde roundtrip tests (PR #1693). Boring is a feature in dependency choices.
The diff
Here is the whole change, before and after. This is the real code from the fix:
// before: consensus/src/crdt.rs
pub fn encode(&self) -> Result<Vec<u8>, CrdtError> {
bincode::serialize(self).map_err(|e| CrdtError::Encode(e.to_string()))
}
pub fn decode(bytes: &[u8]) -> Result<Self, CrdtError> {
bincode::deserialize(bytes).map_err(|e| CrdtError::Decode(e.to_string()))
}
// after: consensus/src/crdt.rs
pub fn encode(&self) -> Result<Vec<u8>, CrdtError> {
postcard::to_stdvec(self).map_err(|e| CrdtError::Encode(e.to_string()))
}
pub fn decode(bytes: &[u8]) -> Result<Self, CrdtError> {
postcard::from_bytes(bytes).map_err(|e| CrdtError::Decode(e.to_string()))
}
# before: consensus/Cargo.toml
bincode = "1.3"
# after
postcard = { version = "1", features = ["use-std"] }
Four files changed: the two call sites, the Cargo.toml line, the deny.toml ignore entry (more on that below), and the lockfile, from which bincode vanished entirely. The full workspace suite stayed green: 521 passed, 0 failed.
The gotcha
The first build failed. postcard 1.1.3 does not enable its use-std feature by default. The default is heapless-cas, aimed at microcontrollers, so postcard::to_stdvec simply does not exist until you ask for it. The compiler helpfully suggested to_vec, which returns a heapless Vec, not what a standard binary wants. One feature flag, one rebuild, green.
Read the feature list, not just the docs front page. That cost twenty minutes and saved a wrong abstraction.
The question that mattered
The API mapping was ten lines. The real work was the compat audit, because postcard and bincode do not produce the same bytes. bincode 1.x writes integers fixed-width and little-endian: a u64 is always eight bytes, even when the value is 7. postcard writes varints: small numbers take one or two bytes, and the width grows with the value. A varint is just an integer encoding that spends bytes in proportion to magnitude. Same struct, different bytes on the wire.
A codec swap is never about the function names. It is about answering three questions: who reads these bytes, where are they stored, and who else speaks this format? I audited every call site. encode and decode are used in two unit tests and one integration roundtrip test, and nowhere else. No blobs are persisted to disk. No other implementation speaks the old format. So the format break is contained: peers must run the same codec, and they do, because both ends are this code. I wrote that verdict into the doc comment so the next person does not have to redo the audit.
The accident
Then I measured, because "smaller" is a claim and claims need numbers. On a mirror of the wire types, built in release mode, the sync blobs came out like this:
One more small ritual, and it is the same one as last week. The advisory had been sitting in deny.toml's ignore list, documented, pointing at issue #265. A conscious decision, not a silent pass: the gate stayed green for new advisories while the old one waited its turn. The fix deleted that entry. Two weeks, two ignores removed.
One remains. Issue #266 is the hickory-proto denial-of-service that arrives through libp2p, and its fix is a transport-layer migration, not a ten-line swap. It is still on the list, and it is not this week's story.
TAKEAWAY // WHAT THE FIX TEACHES
- Audit the bytes, not the API. The function names mapped one to one; the wire format did not.
- Unmaintained is a schedule, not an emergency. An ignore list with a tracking issue is the honest way to hold it until its week comes.
- Read the feature flags. postcard's
use-stdis not on by default; the compiler told me exactly once. - Measure before and after. A varint-vs-fixed-width difference of 4x was hiding inside a security chore.
- Write the compat verdict into a comment. The next reader should not have to redo the audit.
The postcard-vs-bitcode argument is still going on r/rust, and I am not settling it here. bitcode claims smaller and faster; postcard won on boring familiarity. For a sync blob between islands, boring was the right call. Related reading: the r/rust threads "Bincode development has ceased permanently" and "bitcode: smallest and fastest binary serializer".