bcrypt vs Argon2: Password Hashing in 2026
Choose the right password hashing algorithm and tune its parameters for production.
Password hashing has exactly one job: make each guess expensive for an attacker who has already stolen your database. General-purpose hashes fail at this by design — a single consumer GPU computes tens of billions of SHA-256 hashes per second, so an 8-character password hashed with SHA-256 falls in minutes. bcrypt and Argon2 exist to change that economic equation, and choosing between them is mostly about understanding what each one makes expensive.
The Threat Model: Offline Cracking
After a breach, the attacker holds hash values and runs guesses locally at hardware speed. Salting (which both algorithms handle automatically, embedding the salt in the output string) prevents rainbow tables and forces per-user cracking, but it does nothing about guessing rate. The guessing rate is set by two levers:
- Time cost — how much computation one hash requires
- Memory cost — how much RAM one hash requires
Memory is the more interesting lever. GPUs have thousands of cores but limited fast memory per core; an algorithm that demands 64 MB per hash cuts GPU parallelism from thousands of concurrent guesses to a handful.
bcrypt: 1999 Design, Still Sound
bcrypt (Provos and Mazières) is built on an intentionally expensive Blowfish key schedule. The cost parameter is logarithmic — cost 12 means 4,096 iterations, and each increment doubles the work:
import bcrypt from 'bcrypt';const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(password, hash);
Its 4 KB internal state resists GPUs better than SHA-family hashes but is far below what modern memory-hard functions demand. Two sharp edges to know:
- 72-byte truncation. bcrypt silently ignores input past 72 bytes, so
correct horse battery staple until byte seventy-two...and a string differing only afterward hash identically. If you pre-hash to work around it, encode the digest as base64 or hex first — raw SHA-256 output can contain NUL bytes, and some bcrypt implementations truncate at the first NUL, collapsing many passwords onto one. - Variant zoo.
$2a$,$2b$,$2y$prefixes reflect historical implementation bugs. Modern libraries emit$2b$; just avoid mixing libraries that disagree.
Argon2: The Memory-Hard Winner
Argon2 won the Password Hashing Competition in 2015 and is OWASP's recommended default. Use the Argon2id variant — it combines Argon2i's defense against side-channel attacks with Argon2d's stronger GPU resistance:
import argon2 from 'argon2';const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // KiB, so 64 MiB
timeCost: 3,
parallelism: 1
});
const ok = await argon2.verify(hash, password);
The three parameters are independent: memoryCost sets RAM per hash, timeCost sets passes over that memory, parallelism sets lanes. OWASP's current minimum is 19 MiB / t=2 / p=1; 64 MiB / t=3 / p=1 is a comfortable server-grade setting. Output is a self-describing PHC string — $argon2id$v=19$m=65536,t=3,p=1$... — so parameters travel with the hash and verification of old hashes keeps working after you raise settings.
Tuning Methodology
Parameters are hardware-dependent, so tune empirically rather than copying numbers:
- Benchmark on production-class hardware and target 200-500 ms per hash for interactive login.
- Test under concurrency. Fifty simultaneous logins at 64 MiB each is 3.2 GB of RAM — memory-hard hashing is a self-inflicted DoS vector if unbounded. Put a semaphore or dedicated queue around hashing.
- Re-evaluate yearly. bcrypt cost 12 today should become 13 when hardware catches up.
Migration Without a Password Reset
You never need to force resets to upgrade hashing:
1. Wrap existing hashes now: store argon2(legacy_hash) and mark the row as wrapped, killing the weak hash at rest immediately.
2. Rehash on login: when a user authenticates, you briefly hold the plaintext — verify against the old scheme, then store a fresh Argon2id hash.
3. Most libraries expose a needsRehash check that compares a stored hash's parameters to your current policy; call it on every successful login and upgrades become automatic.
Common Mistakes
- Using fast hashes (MD5, SHA-x, even salted) for passwords at all
- Rolling your own verification with a non-constant-time string comparison
- Storing the pepper in the same database as the hashes (keep it in a KMS or env secret, applied as an HMAC over the password before hashing)
- Setting parallelism high on a busy web server and starving request threads
- Confusing password hashing with session security — a perfect Argon2 setup does nothing if session tokens are guessable
The Decision
New system: Argon2id at OWASP parameters or above. Existing bcrypt at cost 12 or more: no urgent migration, but wire up rehash-on-login so the fleet upgrades itself. FIPS-constrained environments: PBKDF2-HMAC-SHA256 with 600,000+ iterations is the compliant fallback. Under no circumstances write your own.
Where scrypt Fits
scrypt (2009) pioneered memory-hardness before Argon2 existed and remains a solid choice, especially where library support is mature — Node's built-in crypto.scrypt, Go's x/crypto. Its parameters are harder to reason about: N, r, and p interact in non-obvious ways, and the commonly recommended N=2^17, r=8, p=1 costs about 128 MB per hash. But a correctly tuned scrypt is not meaningfully weaker than Argon2id. If your platform ships scrypt for free while Argon2 would pull in a native dependency, choosing scrypt is a defensible engineering call.
Password quality still matters more than the hash: generate strong random passwords with the Password Generator at sdk.is/password-generator.