Choosing a hashing algorithm: MD5 vs SHA-256 vs bcrypt vs Argon2
"Which hash function should I use?" doesn't have one answer because hashing solves at least three unrelated problems: checking file integrity, detecting duplicate content, and storing passwords. Using a fast general-purpose hash for the third one is one of the most common — and most damaging — security mistakes in web development.
MD5: fast, broken, still fine for one thing
MD5 produces a 128-bit digest and is fast enough to hash gigabytes per second. It is cryptographically broken: collisions (two different inputs producing the same hash) can be generated deliberately, which means MD5 must never be used anywhere an attacker could benefit from crafting a collision — digital signatures, certificate fingerprints, or anything security-relevant.
It's still perfectly fine for non-adversarial integrity checks: verifying that a file downloaded correctly, generating a cache key from content, or deduplicating uploads where nobody is trying to fool you on purpose. If a stranger could feed the input, don't use MD5 for anything that matters.
SHA-256 (and SHA-3): the general-purpose default
SHA-256 is part of the SHA-2 family, produces a 256-bit digest, has no known practical collision attacks, and is the right default for:
- Verifying software downloads and checksums.
- Generating deterministic IDs from content.
- Building Merkle trees / blockchains.
- HMAC-based message authentication (`HMAC-SHA256`) for API request signing.
But SHA-256 is still *fast*, which is exactly the wrong property for password storage: a modern GPU can compute billions of SHA-256 hashes per second, so if a database of `SHA256(password)` leaks, an attacker brute-forces most passwords in hours. Speed is a feature for checksums and a vulnerability for passwords.
Why passwords need a different kind of function
Password hashing functions are deliberately slow and deliberately expensive in memory, so that the attacker's brute-force cost scales with yours. They also build in salting automatically, so two identical passwords never produce the same stored hash, which defeats precomputed rainbow tables.
bcrypt: the safe, boring default
bcrypt has been battle-tested since 1999, is available in essentially every language, and its cost factor is tunable:
const bcrypt = require("bcrypt");
const hash = await bcrypt.hash(password, 12); // cost factor 12
const ok = await bcrypt.compare(candidate, hash);Its main limitation is a fixed, relatively small memory footprint, which makes it somewhat easier to accelerate with custom ASIC or GPU hardware compared to memory-hard alternatives. It also silently truncates inputs longer than 72 bytes in most implementations — worth knowing if you allow very long passphrases.
Argon2: the current recommendation
Argon2 won the 2015 Password Hashing Competition and is the algorithm OWASP currently recommends first. `Argon2id` (the hybrid variant) resists both GPU cracking (via a tunable memory cost) and side-channel attacks:
const argon2 = require("argon2");
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // ~19 MB
timeCost: 2,
parallelism: 1,
});
const ok = await argon2.verify(hash, candidate);The memory-hardness is the point: cracking hardware that's cheap for SHA-256 or even bcrypt gets much more expensive when every guess needs tens of megabytes of RAM, because you can't parallelize that cheaply on a GPU the way you can with pure compute.
PBKDF2: acceptable, mostly legacy
PBKDF2 is still FIPS-approved and shows up in older systems and some compliance-driven environments. It's tunable via iteration count but has no memory-hardness at all, making it the weakest of the three password-specific options against modern GPU cracking. Use it only when a compliance requirement demands it; otherwise prefer Argon2id, then bcrypt.
Quick decision table
| Use case | Algorithm | |---|---| | File integrity / checksums | SHA-256 | | Deduplication, cache keys | SHA-256 or even MD5 | | API request signing (HMAC) | HMAC-SHA256 | | Password storage (new project) | Argon2id | | Password storage (existing bcrypt system) | Keep bcrypt, cost ≥ 12 | | Compliance mandates PBKDF2 | PBKDF2-HMAC-SHA256, ≥ 600,000 iterations | | Digital signatures / certificates | SHA-256 or SHA-3, never MD5/SHA-1 |
The mistake to actually avoid
The single most common real-world error is not "which algorithm" but "hashing passwords with a general-purpose, unsalted, fast hash" — `SHA256(password)` or worse, `MD5(password)`, stored directly in a database column. If you inherit a system doing this, migrating to bcrypt/Argon2 on next login (re-hash on successful auth, replace the stored value) is a safe, incremental fix that doesn't require a forced password reset for every user.
Whatever you choose, never write your own hashing scheme, never use a fast hash for passwords "just this once," and always use a library-provided random salt rather than a fixed or derived one — the salt is not a secret, but it must be unique per password.