MD5, SHA-1 or SHA-256: choosing a hash function
MD5 is broken and still useful. SHA-256 is the default and still not a password hash. The distinction is what the hash is protecting against.
Last updated 30 July 2026
A hash function turns any input into a fixed-length fingerprint. Two properties make it useful: the same input always produces the same output, and finding a different input with the same output should be infeasible. When people say an algorithm is "broken", they mean that second property has failed — and whether that matters depends entirely on whether an attacker is involved.
The three you will actually meet
MD5 (128-bit). Collisions can be produced on ordinary hardware in seconds. That makes it unusable anywhere an adversary controls the input: signatures, certificates, deduplicating untrusted uploads, integrity checks on files someone else supplied. It remains perfectly serviceable as a non-adversarial checksum — a cache key, a "has this local file changed" marker, matching your own download against a vendor's published sum when your threat model is a corrupted transfer rather than a hostile mirror.
SHA-1 (160-bit). Collisions have been demonstrated since 2017 and are now cheap enough to be routine. Git still uses it internally for object addressing, which is fine for the same reason MD5 checksums are fine: Git is not defending against an attacker who controls both sides of a collision. For anything new, there is no reason to choose it — SHA-256 is available everywhere and no slower in any way you will notice.
SHA-256 (256-bit, part of the SHA-2 family). No practical collision attack exists. This is the correct default for content addressing, integrity verification, signature schemes and anything where the input might be hostile. SHA-512 is not more "secure" for these purposes in any way that matters; it is faster on some 64-bit hardware and produces a longer digest.
The job a hash should never be given
Storing passwords. This is the one mistake that turns a database leak into an account-takeover event, and it survives because a hash looks like the right tool.
The problem is speed. SHA-256 is designed to be fast, which means an attacker with a leaked table can test billions of candidate passwords per second on a GPU. Adding a salt stops precomputed rainbow tables and does nothing about that rate. Password hashing needs an algorithm that is deliberately expensive and memory-hard: Argon2id for anything new, bcrypt or scrypt where Argon2 is unavailable, tuned so a single verification takes a noticeable fraction of a second.
The same logic rules out plain hashes for API keys you expect to be brute-forced against, but not for keys you generate yourself at full entropy — a 256-bit random token has nothing to guess, so SHA-256 of it is a fine lookup value.
When you want a hash and want it keyed
Verifying that a message came from someone holding a shared secret is not a hash problem, it is an HMAC problem — HMAC-SHA256, not SHA256(secret + message). The naive concatenation is vulnerable to length-extension attacks against SHA-1 and SHA-2, and webhook signature verification is exactly where this shows up in practice. If you are checking a X-Signature header from a payment provider, you are almost certainly meant to be computing an HMAC.
Comparing digests correctly
Two small things that go wrong even when the algorithm choice is right:
- Compare in constant time. A byte-by-byte comparison that returns early leaks, through timing, how much of a signature you got right. Server-side, use the platform's
timingSafeEqual. - Compare the same encoding. The same digest written as hex and as Base64 are different strings. Hex is lowercase by convention but not by rule, so normalise case before comparing text forms.
What a hash cannot tell you
A matching hash proves two byte sequences are identical. It says nothing about whether either of them is trustworthy — a virus with a correctly published SHA-256 is still a virus. Integrity and authenticity are different properties, and only the second one needs a key.
Identifiers are a different problem
If what you actually need is a unique value rather than a fingerprint of content, hashing is the wrong tool — a UUID is the right one. Version 4 UUIDs come from a cryptographically secure random source and carry no information about what they identify, which is usually what you want in a URL. Hashing an email address to produce a "private" identifier does not work: the input space is small enough to brute-force, so the hash is reversible in practice.