MD5 has been around since 1992 and it refuses to die. You'll find it in legacy databases, file integrity checks, cache keys, and a surprising number of authentication systems that really should know better. It's worth understanding exactly what it does, because the difference between a fine use and a dangerous one is bigger than most people assume.
What MD5 actually is
MD5 is a cryptographic hash function. You feed it any input — a string, a file, a blob of bytes — and it spits out a fixed 128-bit value, usually written as a 32-character hexadecimal string. The same input always produces the same output, and even a one-byte change scrambles the result completely.
md5("hello") = 5d41402abc4b2a76b9719d911017c592
md5("hello!") = f8bd8f4a5d0f9b4e... (totally different)That's the whole idea: a compact, deterministic fingerprint of some data. The problem is the word "cryptographic." MD5 was designed to be collision-resistant, and it isn't anymore. Researchers have been able to generate two different inputs that produce the same MD5 hash since the mid-2000s, and these days you can do it on a laptop.
When MD5 is fine
Broken collision resistance doesn't make MD5 useless. It makes it unsuitable for anything security-sensitive. For non-adversarial jobs it's fast, tiny, and perfectly serviceable.
- Cache keys and deduplication. Hashing a request URL or a set of parameters to build a cache key is a great fit. Nobody's attacking your Redis keys, and you just want a short deterministic identifier.
- Detecting accidental file changes. Comparing MD5 checksums to see if a file changed during a copy or upload works well against corruption and transmission errors.
- Bucketing and sharding. Using the first few hex characters of an MD5 to distribute data across partitions is cheap and even.
- Content-addressable lookups where the inputs aren't controlled by an attacker.
The common thread: nobody is deliberately trying to forge a collision. If that assumption holds, MD5's speed is a feature.
When to absolutely not use MD5
Here's the list that gets people fired:
- Password storage. Never. Use bcrypt, scrypt, or Argon2. MD5 is fast, which is the opposite of what you want for passwords — attackers can compute billions of MD5 hashes per second on a GPU. Salting it doesn't fix the speed problem.
- Digital signatures and certificates. Collision attacks have been used to forge certificates in the wild. Use SHA-256 or better.
- Verifying downloads against tampering. An attacker who controls the file can craft a malicious version with the same MD5. Use SHA-256 for security-relevant integrity checks.
- Any integrity check where an adversary is in the loop. Message authentication, token generation, anti-tamper — all off the table.
Common gotchas
Encoding matters
The bytes you hash depend on the string encoding. "café" in UTF-8 produces a different hash than the same word in Latin-1, because the underlying bytes differ. If two systems disagree on encoding, their hashes won't match and you'll spend an afternoon confused. Always normalise to a known encoding — UTF-8 is the sane default — before hashing.
Trailing whitespace and line endings
A file with Windows CRLF line endings hashes differently from the same file with Unix LF. This bites teams comparing checksums across operating systems constantly. Same goes for a stray trailing newline your editor added.
Salting doesn't make MD5 secure
People sometimes think md5(salt + password) is acceptable. It slows down precomputed rainbow tables, sure, but it does nothing about MD5's raw speed. A modern GPU chews through salted MD5 hashes almost as fast as unsalted ones. Salting is necessary but not sufficient — the algorithm itself is the problem.
Truncation
If you shorten an MD5 to fit a database column, you increase collision probability. That might be fine for a cache key with a fallback, but be honest with yourself about the birthday problem before you lop off characters.
A quick worked example
Say you want a stable cache key for a set of query parameters. Order doesn't matter to the caller but does to the hash, so sort first:
// Node.js
const crypto = require('crypto');
function cacheKey(params) {
const canonical = Object.keys(params)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
return crypto
.createHash('md5')
.update(canonical, 'utf8')
.digest('hex');
}
cacheKey({ page: 2, sort: 'name' });
// -> consistent regardless of key order
// "page=2&sort=name" hashed as UTF-8Note the explicit utf8 and the canonicalisation step. Those two details save you from mismatches when the same logical request arrives with keys in a different order or a different encoding.
In Python the equivalent is just as short:
import hashlib
def cache_key(canonical: str) -> str:
return hashlib.md5(canonical.encode('utf-8')).hexdigest()When you just need to hash a value by hand — checking a checksum, debugging why two keys don't match, or sanity-checking your implementation — it's quicker to paste it into a MD5 Hash Generator than to open a REPL. Handy for verifying that your code produces the same digest as some other system.
The short version
MD5 is a fast, compact fingerprint that's completely broken for anything an attacker touches. Reach for it when you need a deterministic identifier and the threat model is "accidents, not adversaries." Reach for SHA-256 the moment security enters the picture, and reach for bcrypt/Argon2 for passwords, no exceptions.
Most of the pain around MD5 comes not from the algorithm but from encoding mismatches and line-ending surprises. Pin down your bytes, and it behaves. Use it for the wrong job, and it's a liability waiting to appear in a post-mortem.
Photo by Chris Ried on Unsplash