MD5 has been "broken" for security purposes for the better part of two decades, and yet it refuses to die. You'll still find it in checksums, cache keys, dedup logic, and countless legacy systems. That's not necessarily wrong — MD5 is fast and produces a compact 128-bit fingerprint. The trick is knowing which jobs it's actually suited for.
What MD5 actually does
MD5 (Message-Digest Algorithm 5) takes an input of any length and produces a fixed 128-bit output, usually written as 32 hexadecimal characters. Feed it the same bytes and you always get the same digest. Change a single bit and the output looks completely different.
For example:
md5("hello") = 5d41402abc4b2a76b9719d911017c592
md5("Hello") = 8b1a9953c4611296a827abf8c47804d7
Same five letters, one capitalisation change, totally different hash. That property — small input change, large output change — is what makes hashes useful as fingerprints.
Where MD5 is genuinely fine
The security community shouting "MD5 is broken" refers to a specific problem: it's vulnerable to collisions and preimage-style attacks that make it unsafe for anything an attacker can manipulate. But plenty of use cases have no adversary at all.
- Cache keys. Hashing a normalised request or query into a short, uniform key is a classic use. You just need a stable identifier, not a secret.
- Deduplication. Detecting duplicate files or records where you control all the inputs. If you're worried about deliberate collisions, that's a different story.
- Non-security checksums. Verifying a download completed without corruption, where the file source is trusted.
- Sharding and partitioning. Distributing keys across buckets evenly. MD5's uniform output is handy here.
- ETags and content addressing (low-stakes). Quick content fingerprints for internal tooling.
In all of these, speed matters and the threat model doesn't include someone crafting malicious inputs. MD5 does the job.
Where MD5 will get you in trouble
Passwords
Never store passwords as MD5 hashes. It's fast, which is exactly the wrong property — an attacker with a leaked database can compute billions of MD5 hashes per second on commodity GPUs. Salting helps a little, but the fundamental issue is speed. Use a purpose-built password hash like bcrypt, scrypt, or Argon2, which are deliberately slow and memory-hard.
Digital signatures and certificates
MD5 collisions are practically achievable. Researchers demonstrated forging a CA certificate years ago. Any signature scheme relying on MD5 is untrustworthy. Use SHA-256 or better.
Integrity against a motivated attacker
If someone can substitute a file and the checksum, MD5 gives you no protection — they can craft a different file with the same digest. For security-relevant integrity, reach for SHA-256 or an HMAC with a secret key.
Common gotchas
Even in the "safe" use cases, MD5 catches people out in predictable ways.
- Encoding mismatches. Hashing a string means hashing its bytes.
md5("café")in UTF-8 differs from Latin-1. Always fix your encoding explicitly, or you'll get "the same string" producing different hashes across systems. - Trailing whitespace and newlines. A file with a trailing newline hashes differently. So does a JSON blob with keys in a different order. Normalise before you hash.
- Hex case and formatting. Some libraries output uppercase, some lowercase; some add colons or spaces. Compare hashes case-insensitively or normalise the format.
- Salting confusion. Adding a salt to an MD5 password hash does not make it secure. It only slows down precomputed rainbow tables. It doesn't fix the underlying speed problem.
A short worked example
Say you want a cache key for a normalised search query. Here's how it looks in a few languages.
Python:
import hashlib
query = " Blue Shoes "
normalised = query.strip().lower() # "blue shoes"
key = hashlib.md5(normalised.encode("utf-8")).hexdigest()
print(key) # 2c9341ca4cf3d87b9e4eb905d6a3ec45
Node.js:
const crypto = require("crypto");
const query = " Blue Shoes ";
const normalised = query.trim().toLowerCase();
const key = crypto.createHash("md5")
.update(normalised, "utf8")
.digest("hex");
console.log(key);
Notice the two things that matter: the strip/trim plus lower normalisation, and the explicit UTF-8 encoding. Skip either and your keys drift apart between environments.
When I'm debugging why two systems disagree on a hash, I don't guess — I paste both inputs into a quick generator to compare byte-for-byte. Our MD5 Hash Generator is handy for that: paste a string, get the digest instantly, and confirm whether that mystery trailing space is the culprit.
Choosing the right tool
A rough decision guide:
- Need a fast, non-secret fingerprint? MD5 or even a non-cryptographic hash like xxHash or CRC32 is fine.
- Need integrity in a trusted pipeline? SHA-256 is barely slower and future-proof.
- Need integrity against attackers, or a keyed check? HMAC-SHA256.
- Storing passwords? Argon2id, then bcrypt or scrypt as fallbacks. Never a bare hash.
The rule of thumb I keep coming back to: if there's no adversary and you just need a stable, uniformly distributed identifier, MD5 is a perfectly reasonable, well-supported choice. The moment security enters the picture, walk away from it.
Bottom line
MD5 isn't evil — it's a hammer that people keep trying to use as a padlock. Use it for cache keys, dedup, and non-security checksums where its speed is an asset. Reach for SHA-256 or a dedicated password hash the instant real security is on the line. And when hashes mysteriously don't match, check your encoding and whitespace first. That's the bug about ninety percent of the time.
Photo by Chris Ried on Unsplash