Run this:
echo -n "password" | md5sum
echo -n "password" | sha1sum
echo -n "password" | sha256sum
You get:
5f4dcc3b5aa765d61d8327deb882cf99 -
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 -
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -
The MD5 vs SHA1 vs SHA256 question looks simple from a distance: longer output, more secure. The real tradeoffs are more nuanced than that.
Broken, But Broken How?
MD5 collisions have been practical since 2008. Generate one in under 10 minutes on a laptop. SHA1 followed in 2017 when Google demonstrated SHAttered, the first practical SHA1 collision. Both are "broken" in the cryptographic sense.
Cryptographic broken means researchers found two different inputs that produce the same output. It doesn't mean the hash is reversible, and it doesn't mean collisions happen during normal operation. You need an attacker actively crafting both files.
| Property | MD5 | SHA1 | SHA256 |
|---|---|---|---|
| Output length | 128 bits / 32 hex | 160 bits / 40 hex | 256 bits / 64 hex |
| Birthday bound | 2^64 | 2^80 | 2^128 |
| Practical collision | 2^18 (seconds on GPU) | 2^63 (expensive but known) | None known |
| Speed (relative) | 1x | ~0.6x | ~0.25x |
| Construction | Merkle-Damgård | Merkle-Damgård | Merkle-Damgård |
| NIST status | Forbidden | Deprecated (2011) | Recommended |
The birthday bound tells you how many hashes you'd need for an accidental collision. An MD5 collision at 2^64 is about 18 quintillion items, not something you hit by accident. Google's entire web index is estimated at roughly 2^56 pages. The practical collision figure (2^18 for MD5) is what an attacker needs intentionally, and it's cheap: a few seconds of GPU time. NIST SP 800-107 covers these recommendations in detail.
The Code
Python, using the built-in hashlib:
import hashlib
text = b"password"
print(hashlib.md5(text).hexdigest())
print(hashlib.sha1(text).hexdigest())
print(hashlib.sha256(text).hexdigest())
# For large files, stream it:
sha256 = hashlib.sha256()
with open("largefile.iso", "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
print(sha256.hexdigest())
Node.js using the crypto module:
import { createHash } from 'node:crypto';
const text = "password";
console.log(createHash('md5').update(text).digest('hex'));
console.log(createHash('sha1').update(text).digest('hex'));
console.log(createHash('sha256').update(text).digest('hex'));
The performance difference only shows up when you hash millions of items, and even then I/O is usually the bottleneck first. The main practical difference is output length: 32, 40, or 64 characters in your database column.
A Detail That Saves Debugging Sessions
MD5, SHA1, and SHA256 all use the Merkle-Damgård construction: they process input in fixed-size blocks and maintain internal state between blocks. This means they share a vulnerability that most developers don't know about.
If you know hash(message) and the length of the message, you can compute hash(message + padding + extra) without knowing the message itself. This is called a length-extension attack, and it breaks any authentication scheme built as hash(secret + data).
This pattern shows up in API authentication code more often than it should. It passes code review because it looks reasonable. The tokens work in testing. Once an attacker figures out the scheme, they can forge messages without ever discovering the secret. The fix is HMAC: HMAC-MD5 with a proper key is still surprisingly secure despite MD5 being "broken."
This doesn't matter for checksums or deduplication. But if you're building anything involving secrets and hashes, Merkle-Damgård matters. SHA3 uses a sponge construction and is immune.
The Real Cost
The strongest argument against MD5 isn't technical. It's that using MD5 signals you haven't thought about security, which becomes a self-fulfilling prophecy when your team stops asking "is this safe here?" over time. But the compliance cost is tangible: PCI-DSS and SOC2 both flag MD5 explicitly. If your system uses MD5 for anything, even file deduplication, it triggers a question during audit.
Compliance audits trigger MD5 migrations regardless of use case. A deduplication system running for years without a single collision still gets flagged. The hash function was fine. But the auditor's checklist doesn't accommodate nuance. The engineering cost wasn't the migration itself; it was the sprint planning, the testing, and the explanation to management.
SHA1 is heading the same direction. NIST deprecated it in 2011, and Git remains the only high-profile holdout (commit hashes are SHA1, though the project is actively working on a transition to SHA256).
What I Reach For
File downloads and published checksums: SHA256. Package managers set the convention. npm, pip, Homebrew, Docker all publish SHA256 checksums. Matching the ecosystem saves your users from checking what you used.
For internal cache keys and deduplication, MD5 is hard to beat. It's fast, the output fits in a URL, and there's no security boundary to worry about. Redis key names, content-addressable storage, duplicate detection. All cases where speed and space matter more than collision resistance.
API tokens and session identifiers can use SHA256 truncated to 128 bits. Rails does this for session IDs. You get enough security for any practical system in a shorter string. Full SHA256 is wasted entropy for something you regenerate regularly.
Passwords: Argon2id. Not SHA256, not SHA256 wrapped in a loop, not "SHA256(salt + password)". All fast hashes prioritize speed, and speed is exactly what you don't want for password storage.
SHA256 is the right default for most things. The skill isn't knowing which one is most secure. It's knowing when security doesn't enter the equation.
You can test each of these yourself on the hash generator page. It supports MD5, SHA1, SHA256, SHA512, and a few others.