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-8

Note 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