Bcrypt vs Argon2 vs Scrypt vs PBKDF2 (2026 Guide)
Choosing a password hashing algorithm is one of the few security decisions that is genuinely hard to reverse once millions of hashes are stored. The good news is that all four of the algorithms developers actually consider in 2026 are still considered acceptable by OWASP under the right configuration. The differences are about defense-in-depth and tuning, not "one is broken and the rest are safe." This guide gives you a decision framework rather than a feature dump, plus the specific parameters and a migration plan you can ship.
The 30-second decision
- Greenfield system, no compliance constraint: Use Argon2id. It is the OWASP-recommended default and the winner of the 2015 Password Hashing Competition.
- Already on bcrypt, or your platform has battle-tested bcrypt libraries: bcrypt is still safe. Tune the cost factor and watch the input-length gotcha below.
- Argon2 unavailable on your platform: Use scrypt as the memory-hard fallback.
- FIPS-140 / regulated environment: Use PBKDF2 with HMAC-SHA-256. It is the only one of the four with broad FIPS validation.
What actually makes a password hash strong
A password hash for storage is not the same thing as a general-purpose hash like SHA-256. A cryptographic hash function is designed to be fast, which is exactly the wrong property for password storage because it lets an attacker try billions of guesses per second on a GPU. Password hashing functions are deliberately slow and tunable. Three properties matter:
- Per-password salt defeats precomputed rainbow tables. All four algorithms salt by default.
- Cost / work factor sets how much CPU time each guess costs. You raise it as hardware improves.
- Memory hardness forces each guess to consume RAM, which neutralizes the massive parallelism advantage of GPUs and ASICs. Argon2 and scrypt have it; bcrypt has a small amount; PBKDF2 has none.
Memory hardness is the dividing line. Because a GPU or ASIC can run thousands of PBKDF2 threads in parallel with almost no memory per thread, an attacker scales out cheaply. Forcing each guess to allocate megabytes of RAM, as Argon2id and scrypt do, breaks that parallelism: the attacker now has to buy memory bandwidth and capacity at scale, which is far more expensive than adding compute cores. That is why, at the same login latency per guess, a memory-hard deployment is dramatically more costly to attack than a PBKDF2 one tuned to the same wall-clock time.
Argon2id: the 2026 default
Argon2 (specified in RFC 9106) comes in three variants. Use Argon2id, the hybrid that resists both GPU cracking and side-channel attacks. OWASP's current recommended baseline is a memory cost of m=19456 (19 MiB), a time cost of t=2 iterations, and parallelism p=1. OWASP lists an equivalent baseline of m=47104 (46 MiB), t=1, p=1 as well; the two trade RAM against CPU for the same strength. If your servers have headroom, raise the memory cost first, since it buys the most resistance per millisecond.
// Node.js, using the argon2 package
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // KiB
timeCost: 2,
parallelism: 1,
});
The parameters are stored inside the encoded hash string, so verification needs no separate config and you can raise costs over time without breaking old hashes.
Bcrypt: still safe, with two real gotchas
Bcrypt has been in production since its 1999 USENIX publication and remains a perfectly acceptable choice. OWASP suggests a work factor (cost) of at least 10, and many teams now use 12 or higher. The cost is logarithmic, so each increment doubles the work. You can read and decode the cost factor of any real hash with the bcrypt hash generator to confirm what your library is actually emitting.
Two gotchas trip up almost everyone:
- The 72-byte truncation. Bcrypt silently ignores everything past the first 72 bytes of input. A 100-character passphrase is no stronger than its first 72 bytes. Worse, naive fixes break things: if you pre-hash with raw SHA-256 to shorten the input, the resulting bytes can contain a null byte, and some bcrypt implementations truncate at the first null. The correct pattern, used by frameworks such as Django, is to pre-hash with SHA-256 and then Base64-encode the digest before feeding it to bcrypt, so there are no null bytes and the input stays well under 72 bytes. (OWASP suggests an HMAC-based variant with a secret pepper for extra defense in depth, but the Base64 step is the part that actually prevents the truncation bug.)
- Library-specific prefixes. Hashes may carry
$2a$,$2b$, or$2y$prefixes from historical bug fixes. Most modern libraries verify all of them, but mixing libraries across languages can surprise you. Test cross-language verification before you rely on it.
Scrypt and PBKDF2: the situational picks
Scrypt (RFC 7914) is memory-hard like Argon2 and is a solid fallback when an audited Argon2 binding is not available on your stack. Its parameters are less intuitive: the CPU/memory cost N (a power of two), block size r, and parallelism p. OWASP's baseline is N=2^17 (131,072), r=8, p=1. Tune N upward as hardware improves.
PBKDF2 (defined in RFC 2898, with guidance in NIST SP 800-132) has no memory hardness, so it leans entirely on iteration count for cost. Use it primarily when FIPS compliance forces your hand. OWASP recommends PBKDF2-HMAC-SHA-256 with at least 600,000 iterations. Because it parallelizes cheaply on GPUs, PBKDF2 needs a much higher iteration count to match the real-world cracking resistance of the memory-hard options.
A silent migration playbook
You cannot re-hash existing passwords without the plaintext, and you should never store plaintext. So you migrate lazily, at login time, with zero user disruption:
- Add a new column or algorithm tag so each stored hash records which algorithm and parameters produced it. Encoded hash strings (Argon2, bcrypt) already embed this.
- On every successful login, after you verify the password against the old hash, immediately re-hash the same plaintext with the new algorithm and overwrite the stored value inside the same request. The user never notices.
- For users who never log in, the safest interim option is to wrap the old hash: treat the existing bcrypt or PBKDF2 output as the input to your new Argon2id call. This upgrades dormant accounts immediately without plaintext, and you unwrap one layer on the next successful login.
- Force a password reset for any accounts still on a truly weak scheme (unsalted MD5/SHA-1) rather than carrying that risk.
Before and after migration, verify you are not weakening anything elsewhere: confirm minimum-length and breach-check policies with a password strength checker. For a broader checklist on the whole storage pipeline, see our guide on how to store passwords securely.
The bottom line
Pick Argon2id for new systems, keep bcrypt if you already have it and tune the cost, fall back to scrypt when Argon2 is missing, and reserve PBKDF2 for FIPS. Whatever you choose, salt every password, store the parameters with the hash, and raise the work factor on a schedule. The algorithm matters less than configuring it well and migrating off anything fast or unsalted.
Frequently Asked Questions
Yes. Bcrypt remains an OWASP-acceptable password hashing algorithm when configured with an adequate cost factor (typically 10 or higher). Its main limitations are the 72-byte input truncation and the absence of strong memory hardness, but for most applications a properly tuned bcrypt deployment is still safe.
Argon2id is a hybrid that combines Argon2d's resistance to GPU cracking attacks with Argon2i's resistance to side-channel timing attacks. RFC 9106 and OWASP recommend Argon2id as the general-purpose default precisely because it defends against both threat models simultaneously, unlike either pure variant. RFC 9106 also requires that any conforming implementation support Argon2id.
Bcrypt ignores any input past the first 72 bytes, so long passphrases are silently truncated. The safe fix, used by frameworks like Django, is to pre-hash the password with SHA-256 and then Base64-encode that digest before passing it to bcrypt. This keeps the input short and avoids null bytes that some bcrypt libraries treat as terminators.
Use PBKDF2 primarily when FIPS-140 compliance requires a validated algorithm, since Argon2 and scrypt usually lack FIPS validation. PBKDF2 has no memory hardness, so OWASP recommends PBKDF2-HMAC-SHA-256 with at least 600,000 iterations to compensate for how cheaply GPUs can attack it.
Yes, using lazy migration. After verifying a user's password against the old hash at login, re-hash the same plaintext with the new algorithm and overwrite the stored value in the same request. Dormant accounts can be upgraded by wrapping the old hash as input to the new function until the user next logs in.