The short answer
For a new system in 2026, use Argon2id. It's memory-hard, so an attacker can't trade cheap silicon for speed the way a GPU farm does against bcrypt and PBKDF2. Keep bcrypt only where a legacy stack already ships it; reach for PBKDF2 only when FIPS-140 forces your hand. The choice is the easy part. The hard part is tuning memory, time, and parallelism to a real latency budget instead of pasting someone's blog defaults.
Why is memory-hardness the whole game?
All three functions do one job: turn a password into a verifier slowly enough that guessing gets expensive. They differ in which resource they make expensive.
PBKDF2 burns CPU. It's HMAC-SHA-256 run a few hundred thousand times, nothing more. bcrypt burns CPU plus a fixed 4 KB of working memory, which was clever in 1999 and is rounding error now. Argon2id burns CPU and a configurable, large amount of RAM, and it forces the attacker to actually hold that RAM for the entire computation.
That last property is what breaks GPU and ASIC cracking. A modern GPU has thousands of cores hanging off a thin, shared memory bus. Hand it PBKDF2 and every core runs flat out: billions of guesses a second. Hand it Argon2id at 256 MiB per guess and the bus chokes. The cores sit idle, starved of RAM they can't all have at once. The defender pays for that memory once per login. The attacker pays for it once per guess, in parallel, millions of times over. The asymmetry is the entire point.
A fair comparison, with the warts
| Property | Argon2id | bcrypt | PBKDF2 |
|---|---|---|---|
| Standard | RFC 9106 (2021) | 1999, Blowfish-based | RFC 2898 / NIST SP 800-132 |
| Memory-hard | Yes, tunable | No (fixed ~4 KB) | No |
| GPU/ASIC resistance | Strong | Moderate | Weak |
| Tunable knobs | memory, time, parallelism | work factor only | iterations only |
| Input length cap | None practical | 72 bytes, silently truncated | None practical |
| OWASP 2026 floor | 19 MiB / t=2 / p=1 | work factor ≥ 10 | 600,000 iters (HMAC-SHA-256) |
| FIPS-140 approved | No | No | Yes |
bcrypt's 72-byte trap earns a callout because it bites real systems. bcrypt is built on Blowfish, whose key schedule uses an 18-element P-box of 32-bit words, exactly 72 bytes, and it only ever reads that much of your input. Everything past byte 72 is discarded. Two passphrases that share their first 72 bytes hash to the same value, which means a long, carefully chosen passphrase can be silently weaker than a short one. The standard fix is to pre-hash with SHA-256 and feed the digest to bcrypt. That's a workaround you have to remember, not a property you get for free.
Credit where it's due. bcrypt has 25 years of battle-testing and no memory-tuning footguns. PBKDF2 is the only one of the three you can run inside a FIPS-validated module, so when a compliance auditor demands FIPS-140, PBKDF2 at 600,000 iterations of HMAC-SHA-256 is the honest answer, even though it's the weakest against a serious GPU rig. Knowing which constraint you're actually under beats reflexively grabbing the "best" algorithm.
How do you tune Argon2id without guessing?
Argon2id takes three numbers: m (memory in KiB), t (time cost / iterations), and p (parallelism / lanes). OWASP's floor is 19 MiB, t=2, p=1. That's a minimum to clear, not a target to aim at. The right way to set the numbers is to fix a latency budget and measure against it.
- Pick a budget. For an interactive login, aim for roughly 0.5 to 1 second of server work. Two seconds annoys users; 5 ms isn't slowing an attacker down.
- Spend on memory first. Memory is what hurts GPUs, so max it before you touch iterations. Decide how much RAM you can afford per concurrent login and set
mto that. Somewhere in 64 to 256 MiB is sane for a server that isn't fielding thousands of logins a second. - Raise time to fill the budget. With memory fixed, bump
tuntil a hash lands inside your latency window on production-class hardware. Measure on the box that runs it, not your laptop. - Match parallelism to cores you'll actually give it.
p=2uses two lanes and speeds the defender's single hash without weakening the function, but only if those cores are free under load.
Then write the parameters down and leave a comment explaining that changing them invalidates every stored verifier. Because it does, and someone will try to "optimize" them in two years with no migration plan.
Where beebeeb runs the heavy hash, and why it's on the server
beebeeb uses Argon2id at 256 MiB memory, 4 iterations, 2 parallelism, well above the OWASP floor and into the range where GPU cracking gets genuinely uneconomic. Those parameters live in the open-source Rust client core, so you can read them in the source instead of taking our word for it. The same configuration backs the recovery path, where a BIP39 recovery code runs through Argon2id to reconstruct the master key, which keeps one hardened setup rather than several that drift apart.
Placement is the interesting part. In a normal login the heavy Argon2id runs on the client, or the raw password reaches the server to be hashed there. beebeeb does neither. Login goes through OPAQUE, an augmented PAKE where the password never leaves your device in any form, plaintext or hashed. The Argon2id work happens server-side as OPAQUE's key-stretching step, applied to a value that already mixes in the protocol's per-user OPRF output. So an attacker who walks off with the entire credential database gets verifier files that cost 256 MiB of Argon2id per guess to attack, not the millisecond-per-guess a naive setup leaks.
That's the architecture worth internalizing. A strong KDF protects you only when it's applied to something the server never sees in the clear. A 256 MiB Argon2id hash of a password that crossed the wire as plaintext is theater. Pairing the KDF with OPAQUE, inside a zero-knowledge model where files are encrypted client-side under AES-256-GCM before they ever reach us, is what makes the parameter strength load-bearing. It's also where the honest line on competitors sits. Proton Drive handles password hashing properly too, and its clients are open source. We don't claim our hashing is stronger; done right, password hashing converges. The real distinction is open-source clients plus EU hosting in Falkenstein, Germany, not a magic KDF.
What to take back to your own system
Pick Argon2id, tune it against a latency budget, lean on memory before time, and treat the parameters as a migration-sensitive constant rather than a knob to fiddle with. Fall back to bcrypt only for legacy compatibility, and pre-hash if you do, to dodge the 72-byte cliff. Fall back to PBKDF2 only when FIPS-140 is non-negotiable, then go big on iterations. And before you spend a week tuning any of this, check whether the password reaches your server at all, because the strongest hash in the world is useless if the plaintext lands in a log line first.
If you'd rather poke at the model than read about it, our security page walks the full crypto stack, and