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.

AlgorithmWidthSpeedDesigned for
CRC3232 bitVery fast, often hardware acceleratedDetecting accidental transmission errors
CRC32C32 bitFast, hardware acceleratedSame, with a different polynomial
FNV-1a32 or 64 bitVery fast, trivial to implementHash table key distribution
MurmurHash332 or 128 bitFast, good avalancheHash tables, Bloom filters
xxHash32, 64 or 128 bitExtremely fast, near memory bandwidthChecksums in high-throughput pipelines
CityHash / FarmHash64 or 128 bitFastIn-process hash tables and sharding
SipHash64 bitModerateKeyed 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 CRC32

Choosing 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.

Hashes inside data structures Choosing a hash: a decision guide

Last refreshed 2026-09-18.