Argon2 vs bcrypt vs scrypt vs PBKDF2

Most advice about password hashing on the internet is a decade old, and it says "use bcrypt." The current OWASP Password Storage Cheat Sheet says something different: Argon2id first, then scrypt, then PBKDF2 where FIPS-140 compliance demands it, and bcrypt for legacy systems only. This guide covers what separates the four, the exact parameters OWASP recommends for each, the bcrypt-specific traps that catch people, and how to migrate without forcing a password reset. Measure the parameters against your own hardware with the password hashing parameter calculator.

TL;DR

AlgorithmOWASP rankMemory-hardUse it when
Argon2id1Yes, tunableDefault choice for anything new.
scrypt2Yes, tunableArgon2id unavailable in your stack.
PBKDF23NoFIPS-140 compliance is required.
bcrypt4Barely (~4 KiB)Legacy systems only.

All four are deliberately slow functions designed for passwords. None of them should be confused with SHA-256 or MD5, which are fast general-purpose hashes and are the wrong tool entirely.

The thing that actually separates them

Every one of these functions is built to be slow, because slowness is the only defence that still works once an attacker has your database. The difference is what resource they force the attacker to spend.

PBKDF2 and bcrypt are essentially CPU-bound. PBKDF2 is a loop around HMAC, and bcrypt uses about 4 KiB of working memory. Both are cheap to parallelise: a GPU with thousands of cores, or a purpose-built ASIC, can run enormous numbers of them at once, and that gap between "slow on your server" and "fast on their hardware" is the whole problem.

Argon2id and scrypt are memory-hard. They require a configurable, substantial block of memory per hash, and memory is the one resource that does not get dramatically cheaper on specialised cracking hardware. An attacker who wants to run ten thousand Argon2id hashes in parallel at 19 MiB each needs roughly 190 GiB of RAM to do it. That constraint is what collapses the attacker's advantage, and it is the reason the ranking looks the way it does.

This is also why bcrypt's demotion is about architecture, not a discovered flaw. Nothing broke. The threat model moved, and an algorithm designed in 1999 around CPU cost is simply not built for a world of commodity GPU clusters.

Recommended parameters

These come from the OWASP Password Storage Cheat Sheet, verified 2026-07-24. Within each algorithm, the listed sets are considered equivalent in strength — pick based on how much memory you can afford per concurrent hash.

Argon2id

Expressed as memory (m), iterations (t) and parallelism (p):

  • m=46 MiB, t=1, p=1
  • m=19 MiB, t=2, p=1
  • m=12 MiB, t=3, p=1
  • m=9 MiB, t=4, p=1
  • m=7 MiB, t=5, p=1

The first two are specified for Argon2id and should not be used with Argon2i.

scrypt

Expressed as minimum memory cost (N), blocksize (r) and parallelism (p):

  • N=2^17 (128 MiB), r=8 (1024 bytes), p=1
  • N=2^16 (64 MiB), r=8 (1024 bytes), p=2
  • N=2^15 (32 MiB), r=8 (1024 bytes), p=3
  • N=2^14 (16 MiB), r=8 (1024 bytes), p=5
  • N=2^13 (8 MiB), r=8 (1024 bytes), p=10

Note the notation: r=8 is the blocksize parameter you pass to the library; the 1024 bytes in parentheses is the resulting block size. Passing 1024 as r is a real and costly mistake.

PBKDF2

  • PBKDF2-HMAC-SHA256 — 600,000 iterations
  • PBKDF2-HMAC-SHA512 — 220,000 iterations
  • PBKDF2-HMAC-SHA1 — 1,400,000 iterations (legacy only)

bcrypt

  • Work factor ≥ 10, and as high as server performance allows.

Whether any of these fit your latency budget depends entirely on your hardware. The parameter calculator measures them on your actual device.

The bcrypt traps

If you are staying on bcrypt for now, two things bite far more often than a wrong cost factor.

The 72-byte limit. Bcrypt silently truncates input at 72 bytes. A long passphrase is not more secure than its first 72 bytes, and because the limit is in bytes rather than characters, non-ASCII text consumes it faster than users expect. Nothing warns you; the extra input simply does not affect the output.

Pre-hashing and password shucking. The obvious fix — SHA-256 the password first, then bcrypt the digest — creates a subtler problem. If an attacker obtains an unsalted SHA-256 hash of the same password from an unrelated breach, they can "shuck" your bcrypt hash by testing candidate digests directly, bypassing much of bcrypt's cost. OWASP's guidance if you must pre-hash is to key it with a secret pepper:

bcrypt(base64(hmac-sha384(data:$password, key:$pepper)), $salt, $cost)

Argon2id and scrypt have no comparable input limit, which removes the need for any of this.

Migrating without a password reset

You cannot transform a bcrypt hash into an Argon2id hash. Hashing is one-way, so the conversion needs the plaintext password — and the only legitimate moment you hold it is during a successful login. That gives you the standard approach:

  1. Store the algorithm and parameters with each hash (modern encoded formats already do this).
  2. On a successful login, verify against the stored algorithm as usual.
  3. If the stored hash uses the old algorithm or weaker-than-current parameters, immediately rehash the plaintext you already have with Argon2id and replace the row.
  4. Track migration coverage. Active users convert within a login cycle or two.
  5. Set a deadline for dormant accounts and force a reset for whatever remains.

The same mechanism handles routine parameter increases, which is why storing parameters per-hash matters more than picking the perfect number today — it turns "migrate the entire user table" into a background process that costs nothing.

There is an alternative: wrap the existing hashes, storing argon2id(bcrypt(password)) so every row is upgraded immediately without waiting for logins. It protects dormant accounts right away, but it permanently couples you to both algorithms and complicates every future migration. Most teams take rehash-on-login unless they have a specific reason to need instant coverage.

Choosing, in one paragraph

Use Argon2id at the highest OWASP parameter set your latency budget allows. If your platform has no trustworthy Argon2id binding, use scrypt. If you are in a FIPS-140 environment, use PBKDF2-HMAC-SHA256 at 600,000 iterations and accept that it is CPU-bound. Use bcrypt only because you already do — and if so, confirm your cost factor is at least 10, check whether the 72-byte truncation affects your users, and plan rehash-on-login. Whatever you pick, store the parameters with each hash and raise them as hardware improves; that habit matters more over a system's lifetime than the choice you make today.

Frequently Asked Questions

Is bcrypt still safe to use in 2026?

Bcrypt is not broken, and an existing deployment at a sane cost factor is not an emergency. But OWASP now classifies it as legacy systems only, behind Argon2id, scrypt and PBKDF2. The reason is not a cryptographic weakness — it is that bcrypt is only mildly memory-hard (about 4 KiB of working memory), which makes it far more parallelisable on GPUs and ASICs than Argon2id or scrypt. If you are choosing an algorithm today, choose Argon2id. If you already run bcrypt at cost 10 or above, plan a migration rather than a fire drill.

What is the difference between Argon2i, Argon2d and Argon2id?

Argon2d uses data-dependent memory access, which resists GPU cracking well but leaks timing information and is therefore vulnerable to side-channel attacks. Argon2i uses data-independent access, which resists side channels but is weaker against time-memory tradeoff attacks. Argon2id is the hybrid — data-independent for the first pass, data-dependent afterwards — and it is the variant you should use for password hashing. It is what OWASP recommends, and it is the default in most modern libraries. Note that OWASP's first two recommended parameter sets are specified for Argon2id and should not be used with Argon2i.

How many PBKDF2 iterations should I use?

OWASP recommends 600,000 iterations for PBKDF2-HMAC-SHA256 and 220,000 for PBKDF2-HMAC-SHA512. PBKDF2-HMAC-SHA1 is listed at 1,400,000 and should be considered legacy. If those numbers seem enormous compared to what your codebase uses, that is the point — the recommended counts climb every few years as attacker hardware improves, and a value hard-coded years ago is almost certainly too low now. Store the iteration count with each hash so you can raise it incrementally instead of migrating everything at once.

Why does bcrypt ignore part of my password?

Bcrypt truncates its input at 72 bytes. Anything beyond that is silently discarded, so a 100-character passphrase provides no more security than its first 72 bytes — and because the limit is in bytes, not characters, multi-byte characters consume it faster than you would expect. The naive workaround, pre-hashing the password with SHA-256 and feeding the result to bcrypt, reintroduces a problem called password shucking. OWASP's guidance if you must pre-hash is bcrypt(base64(hmac-sha384(data:$password, key:$pepper)), $salt, $cost). Argon2id and scrypt have no comparable input limit.

How do I migrate from bcrypt to Argon2id without resetting passwords?

Rehash on successful login. You cannot convert a bcrypt hash into an Argon2id hash directly — you need the plaintext password, and the only moment you legitimately have it is when the user logs in. So: on a successful bcrypt verification, immediately hash the same plaintext with Argon2id, replace the stored hash, and record which algorithm each row uses. Active users migrate transparently within a login cycle or two. For accounts that never come back, set a deadline and force a reset. The wrapping approach — running Argon2id over the existing bcrypt hashes — works for immediate protection without waiting for logins, but it permanently couples you to both algorithms, so most teams prefer rehash-on-login.

Do I still need a salt?

Yes, but you almost certainly are not generating one yourself. All four algorithms require a unique random salt per password, and every modern implementation generates one automatically and embeds it in the encoded hash string alongside the parameters. That is why an Argon2id or bcrypt hash is a long string with dollar-delimited fields rather than a bare digest — it carries the algorithm, the parameters and the salt with it. If you find yourself manually managing salts, or reusing one, something is wrong. A pepper (a secret held outside the database) is a separate, optional defence and is not a substitute.