MD5 has a weird status in software. It's cryptographically broken, security teams flag it in audits, and yet you'll still find it running quietly in codebases everywhere. That's not always a mistake. MD5 is fast, produces a compact fixed-length output, and works fine for a handful of non-security jobs. The trouble starts when people forget which category their use case falls into.
What MD5 actually is
MD5 (Message-Digest Algorithm 5) takes any input and produces a 128-bit hash, usually written as 32 hexadecimal characters. Feed it the same bytes and you always get the same digest. Change a single bit of input and the output looks completely different.
md5("hello") = 5d41402abc4b2a76b9719d911017c592
md5("hello.") = 8a4feb2043b93a2e0f6c6db85c9d1b0eIt's a one-way function: you can't reverse the digest back into the original input. That property is what makes hashing useful in the first place. What MD5 can't reliably do anymore is guarantee that two different inputs won't produce the same digest.
The collision problem, plainly
A collision is when two different inputs hash to the same value. For any hash function collisions exist mathematically — the input space is infinite and the output is only 128 bits. The difference between a secure and a broken hash is how hard it is to deliberately find one.
For MD5, it's trivially easy. Researchers have generated colliding inputs for years, and there are documented cases of forged certificates built on MD5 collisions. So the moment your use of MD5 depends on "an attacker can't craft a matching input," you've picked the wrong tool.
Where you should never use MD5
- Password storage. Use bcrypt, scrypt, or Argon2. These are deliberately slow and salted. MD5 is fast, which is exactly what you don't want when someone is brute-forcing a leaked database.
- Digital signatures or certificates. Collision attacks make forgery feasible.
- Any integrity check against a malicious adversary. If someone benefits from swapping a file for one with the same hash, MD5 won't stop them.
Where MD5 is genuinely fine
Plenty of tasks only care about accidental changes, not deliberate attacks. For those, MD5's speed is an asset rather than a liability.
- Detecting accidental corruption. Verifying a file downloaded without bit-flips is a classic, valid use. If a byte got mangled in transit, the digest won't match.
- Cache keys and deduplication. Hashing a request payload or a chunk of content to build a stable lookup key works well. Collisions between benign inputs are astronomically unlikely by chance.
- Change detection. Storing the MD5 of a config file or an asset so you can skip work when nothing changed.
- ETags and content addressing in internal systems where you control both ends.
The mental test I use: if a motivated attacker could benefit from a collision, don't use MD5. If the only failure mode is random chance, it's fair game — though SHA-256 is a reasonable default even here if you want to future-proof.
A short worked example
Say you're caching the result of an expensive image transformation. The cache key should reflect the source URL plus the transform parameters. MD5 gives you a short, filesystem-safe key:
import hashlib
def cache_key(url, width, height, fmt):
raw = f"{url}|{width}x{height}|{fmt}"
return hashlib.md5(raw.encode("utf-8")).hexdigest()
key = cache_key("https://cdn.example.com/cat.png", 200, 200, "webp")
# 'e2b4c1...' -> use as a cache filename or Redis keyNothing here depends on collision resistance. Two different parameter sets colliding by accident is vanishingly improbable, and even if it happened the worst case is one stale cached image — not a breach. That's the sweet spot for MD5.
If you just need to eyeball a digest or double-check what your code is producing, you can paste a string into our MD5 Hash Generator and compare the output side by side with your implementation. It's handy when a hash mismatch is driving you up the wall and you want to rule out an encoding bug.
Common gotchas
Encoding mismatches
The single most common source of "why doesn't my hash match?" is inconsistent string encoding. Hashing `"café"` as UTF-8 versus Latin-1 produces completely different digests because the byte sequences differ. Always be explicit about encoding, and hash bytes, not strings, whenever you can.
Line endings and whitespace
Hashing file contents across platforms bites people. A `\r\n` on Windows versus `\n` on Linux changes every hash. Normalise before hashing if you need cross-platform stability.
Uppercase vs lowercase hex
Some libraries emit uppercase hex, others lowercase. `5D41402A...` and `5d41402a...` represent the same digest but won't match a naive string comparison. Normalise the case before comparing.
Hashing large inputs
Don't load a 2 GB file into memory to hash it. Stream it in chunks:
def md5_file(path, chunk=65536):
h = hashlib.md5()
with open(path, "rb") as f:
for block in iter(lambda: f.read(chunk), b""):
h.update(block)
return h.hexdigest()Treating a truncated hash as unique
People sometimes chop an MD5 down to 8 characters for shorter keys. That drastically increases collision odds. If you need something short, understand the birthday-problem math before you commit to it.
The bottom line
MD5 isn't evil, it's just misapplied a lot. It's a fast, compact checksum that's perfect for cache keys, deduplication, and catching accidental corruption. It is completely unsuitable anywhere an attacker has an incentive to forge a match — passwords, signatures, and security-critical integrity checks. Keep those two worlds separate and MD5 stays a useful tool rather than a liability. When in doubt about the boundary, reach for SHA-256; it costs you almost nothing and removes the argument entirely.
Photo by Chris Ried on Unsplash