Non-cryptographic hashes: CRC32, FNV and xxHash
CRC32 for error detection, FNV and MurmurHash for hash tables, xxHash and CityHash for speed, and the one rule that separates them from cryptographic hashes.
Fast hashes protect nothing
A non-cryptographic hash is built for speed and distribution, not for resistance. It is trivial to construct inputs that collide, so it must never be used to detect tampering or to authenticate anything.
| Algorithm | Width | Speed | Designed for |
|---|---|---|---|
| CRC32 | 32 bit | Very fast, often hardware accelerated | Detecting accidental transmission errors |
| CRC32C | 32 bit | Fast, hardware accelerated | Same, with a different polynomial |
| FNV-1a | 32 or 64 bit | Very fast, trivial to implement | Hash table key distribution |
| MurmurHash3 | 32 or 128 bit | Fast, good avalanche | Hash tables, Bloom filters |
| xxHash | 32, 64 or 128 bit | Extremely fast, near memory bandwidth | Checksums in high-throughput pipelines |
| CityHash / FarmHash | 64 or 128 bit | Fast | In-process hash tables and sharding |
| SipHash | 64 bit | Moderate | Keyed hash tables, DoS resistance |
CRC is an error detector, not a security control
CRC computes a polynomial division over the message. It reliably detects the burst errors that appear on a noisy link, and it detects nothing at all against an attacker who can recompute it.
import zlib, binascii
data = b"frame payload"
crc = zlib.crc32(data)
print(f"{crc:08x}") # a 32-bit value, cheap to compute
# transmitted as 4 little-endian bytes appended to the frame
frame = data + crc.to_bytes(4, "little")
def verify(frame):
body, tail = frame[:-4], frame[-4:]
return zlib.crc32(body) == int.from_bytes(tail, "little")
assert verify(frame)# CRC32 is not a checksum for downloads: it is far too weak
python -c "import zlib,sys; print('%08x' % zlib.crc32(open('file.bin','rb').read()))"
# for downloads use SHA-256, not CRC32Choosing one for a hash table
# FNV-1a 64-bit, written out so the mixing is visible
def fnv1a_64(data: bytes) -> int:
h = 0xCBF29CE484222325
for b in data:
h ^= b
h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
return h
for key in (b"user:1", b"user:2", b"user:3"):
print(key, fnv1a_64(key))- Use a seeded hash for hash tables that face untrusted input, or an attacker can force every key into one bucket and turn O(1) into O(n).
- SipHash and xxHash with a random seed are the standard answers. Python, Rust and Java all randomise their string hash by default for this reason.
- Avalanche matters more than speed for table distribution: one changed input bit should change about half the output bits.
- xxHash is the fastest mainstream choice, but it is not collision resistant and never signs anything.
⚠️
A fast unseeded hash plus attacker-controlled keys is a denial-of-service vector. If the keys come from a request body or a query string, use a keyed hash or a table implementation that randomises its seed.
FAQ
Can I use CRC32 to verify a download?
It catches accidental corruption, which is all it claims. Against deliberate modification it is useless, because an attacker recomputes the CRC trivially. Use SHA-256 and a signature.
Is FNV good enough for a Bloom filter?
FNV alone gives weak distribution at scale. Combine two hash values to synthesise k hashes, or use MurmurHash3 or xxHash, which mix far better.
Related
Hashes inside data structures Choosing a hash: a decision guide
Last refreshed 2026-09-18.