2026-01-08 · internals · ~5 min read

Picking BLAKE3 over xxh3

Frostvex through 0.2.4 hashed chunks with xxh3-128. It was fast (~10 GB/s on a single core), it was small (the chunk store keys were 16 bytes), and it was very, very wrong for what we were using it for.

0.2.5 switched to BLAKE3-256. This post is about why I dragged my feet on it for a week, what convinced me, and what the real-world impact looks like.

The problem with xxh3

xxh3 is a beautiful piece of engineering. It is also, very explicitly, not a cryptographic hash. The xxhash README is unambiguous about this. The collision space is large enough that randomly chosen inputs essentially never collide — but adversarially chosen inputs can collide trivially.

The case I cared about was not adversarial. Frostvex assumes peers are trusted; nobody is trying to engineer a malicious chunk that hashes the same as a legit one. The problem was a different kind of failure mode — silent corruption that flips bits in transit, then the receiver re-hashes the chunk and finds a "match" that isn't really a match.

For 128-bit hash and randomly distributed bit errors, the probability of a fake match is around 2^-128, which is mathematically negligible. But in practice, bit errors aren't randomly distributed — they cluster. A power glitch on a NIC can flip a whole burst of bits in a way that, once the hash is computed, looks plausible. xxh3's internal mixing is good but it isn't designed to resist this case.

What BLAKE3 buys

BLAKE3 is a cryptographic hash with a tree structure. It's also fast — about 2 GB/s on a single core, 8+ GB/s with SIMD. That's slower than xxh3, but well above any disk we're going to hit, and well above any network we'd run frostvex over.

The thing I didn't initially appreciate is that the tree structure of BLAKE3 is a perfect fit for what we want to do anyway. Internally, BLAKE3 already chunks input into 1024-byte blocks and hashes them as a Merkle tree. If you keep the intermediate hashes, you can verify a sub-region of a file without re-hashing the whole file. That's what frostvex's parity layer does.

So the choice wasn't "slower hash for safety". It was "slower hash that lets me delete a layer of bookkeeping I was doing on top of xxh3". Net code went down.

Why a week

Honestly? Two reasons.

First, I'd written the existing chunk store assuming 16-byte keys. The migration to 32-byte keys touched the manifest, the on-disk store layout, and a few wire-format spots. Doable, but tedious — and on-disk format migrations have to be reversible.

Second, and embarrassingly: I'd benchmarked BLAKE3 once a year ago and remembered it as "much slower than xxh3." That number was correct in 2023; the SIMD path landed in 2024 and changed the tradeoff. Always re-bench when the constant of comparison has changed.

The numbers

operation0.2.4 (xxh3)0.2.5 (BLAKE3)
chunk hash, 128 KB, single core13 µs62 µs
full pool hash, 88 MB / 244 files76 ms140 ms
strict verify, same pool110 ms180 ms
chunk store key size16 bytes32 bytes
integrity guaranteestatisticalcryptographic

The user-visible cost is a couple hundred milliseconds on bulk operations and ~10% bigger manifests on disk. The benefit is that strict verify actually means something now.


If you're considering a similar switch in your own project, the BLAKE3 reference paper is short and worth reading.