bcrypt is from 1999. Your passwords deserve better
Every password hashing comparison reaches the same question: “bcrypt works fine, why change?”
It does work fine. Nobody is saying bcrypt is broken. But “not broken” and “the right choice for a new project in 2026” are different things. argon2id is the latter.
bcrypt was designed for 1999
Section titled “bcrypt was designed for 1999”bcrypt was published in 1999 by Niels Provos and David Mazières. It was a significant improvement over MD5 and DES-based crypt — the Blowfish-based key schedule made it expensive to brute-force, and the adaptive cost factor meant you could increase iterations as hardware got faster.
Here’s the thing: in 1999, a “fast computer” was a 500 MHz Pentium III. GPUs didn’t exist as general-purpose compute devices. ASICs were a concept from the semiconductor industry, not a consumer product. bcrypt’s threat model was “an attacker with a lot of CPUs.” It solved that problem well.
Then in 2010, Nvidia launched CUDA, and everything changed. A modern GPU can perform billions of SHA-256 hashes per second. Bitcoin mining built an entire industry around ASICs that hash at hundreds of terahashes per second. The threat landscape shifted from “many CPUs” to “specialized hardware that’s orders of magnitude faster and cheaper per hash.”
bcrypt is CPU-hard but not memory-hard. It needs a few kilobytes of memory per hash. A GPU with 24 GB of VRAM can run thousands of bcrypt hashes in parallel without breaking a sweat. The cost factor slows it down, but you’re fighting a hardware arms race by turning a dial — and the dial has a ceiling, because every login also pays that cost.
argon2id was designed for 2015
Section titled “argon2id was designed for 2015”argon2id is the winner of the Password Hashing Competition (PHC) — a 2013–2015 academic competition that attracted 24 submissions from cryptographers worldwide, evaluated by a panel including Bruce Schneier and Colin Percival. It was designed from the ground up to resist the threat landscape that bcrypt couldn’t anticipate: GPU and ASIC attacks.
The key innovation is memory-hardness. argon2id requires a configurable amount of RAM to compute a single hash. Not a few kilobytes — tens of megabytes. This doesn’t matter on a server doing one login at a time. It matters enormously on a GPU.
A GPU is fast because it has thousands of parallel cores. But it has limited memory bandwidth. If each hash requires 19 MiB of memory, a GPU with 24 GB of VRAM can only run ~1,200 hashes in parallel — and the memory bandwidth becomes the bottleneck, not the compute cores. The GPU’s entire advantage (massive parallelism) is neutralized.
ASICs are worse off. You can build a chip that computes bcrypt hashes at ludicrous speed because bcrypt needs almost no memory. Building an ASIC with 19 MiB of fast SRAM per hash core is economically impractical — the silicon cost per hash skyrockets. Memory-hardness turns a compute problem into a memory problem, and memory is expensive in silicon.
Three knobs, not one
Section titled “Three knobs, not one”bcrypt has one tunable parameter: the cost factor (iterations). You turn it up as hardware gets faster. That’s it.
argon2id has three:
- Memory cost — how much RAM each hash requires. Measured in KiB. This is the primary defense against GPUs and ASICs.
- Time cost — how many iterations the memory-hard function runs. More iterations = more passes over the memory buffer.
- Parallelism — how many parallel lanes the computation uses. This lets you tune for the number of cores on your server.
This means you can tune argon2id for the hardware you actually run on. If your server has 4 cores and 4 GB of RAM, you pick parameters that make login take ~100ms without starving other processes. If you’re on a 32-core machine with 64 GB, you can afford higher memory cost. bcrypt’s single dial can’t express “use more memory” at all — it doesn’t have that concept.
What Dulak actually uses
Section titled “What Dulak actually uses”Here’s the implementation in src/server/auth.ts:
export const hashPassword = (password: string) => Bun.password.hash(password, { algorithm: "argon2id", memoryCost: 19456, timeCost: 2, });That’s it. No abstraction, no wrapper class, no config file. Bun.password.hash is built into Bun — zero dependencies.
The parameters:
- memoryCost: 19456 — 19 MiB per hash. This matches the OWASP-recommended argon2id configuration (m=19456, t=2, p=1 as one of two accepted configurations).
- timeCost: 2 — two passes over the memory buffer. OWASP recommends 1–2; Dulak uses 2 for a small additional safety margin.
- parallelism: 1 (Bun’s default when not specified) — single-lane computation, appropriate for a web server where logins are sequential per request.
Verification is equally simple:
export const verifyPassword = (password: string, hash: string) => Bun.password.verify(password, hash);The algorithm and parameters are encoded in the hash string itself ($argon2id$v=19$m=19456,t=2,p=1$...), so verification doesn’t need to be told which parameters were used — it reads them from the hash. This means you can change parameters later and old hashes still verify correctly. You re-hash on next login.
The cost in practice
Section titled “The cost in practice”19 MiB of memory per hash means: if 10 users log in simultaneously, that’s 190 MiB of RAM. On a server with even 1 GB of memory, this is a non-issue. On a $5 VPS with 512 MB, it’s still fine — logins are rare events, not per-request operations.
The time cost: with these parameters, a single argon2id hash takes roughly 50–100ms on modern hardware. That’s the point — password hashing should be slow. A 100ms login delay is invisible to a user and devastating to an attacker trying billions of combinations. bcrypt at cost factor 12 takes a similar amount of time. The difference is that argon2id’s 100ms also consumed 19 MiB of memory, while bcrypt’s 100ms consumed ~4 KB.
When bcrypt is fine
Section titled “When bcrypt is fine”Let’s be clear: if you have an existing system with bcrypt, do not panic-migrate. bcrypt at cost factor 12+ is still resistant to offline attacks with reasonable password entropy. The sky is not falling. Your bcrypt hashes are not “insecure” — they’re just not optimal.
The pragmatic approach for existing systems:
- Keep bcrypt for existing hashes.
- Use argon2id for new passwords.
- When a user with a bcrypt hash logs in, verify with bcrypt, then re-hash with argon2id and update the stored hash.
- Over time, your user base migrates naturally — no forced password resets, no downtime.
This is called password hash migration on login, and it’s the standard recommendation. You don’t need a “migration project.” You need one if statement in your verify path.
Why not just use bcrypt with a higher cost factor?
Section titled “Why not just use bcrypt with a higher cost factor?”Because turning up bcrypt’s cost factor is a losing game. Every doubling of the cost factor doubles login time for your users and doubles the attacker’s cost per hash. But the attacker has a GPU with thousands of parallel cores — doubling their cost per hash barely matters when they’re running 10,000 in parallel. Meanwhile, your user is now waiting 400ms instead of 200ms for a login.
argon2id’s memory cost scales differently. Increasing memory cost from 19 MiB to 64 MiB doesn’t just make each hash slower — it reduces the number of hashes a GPU can run in parallel by 3.4×, because VRAM is the constraint. You’re not just increasing the cost per hash; you’re reducing the attacker’s throughput. That’s a fundamentally different defense.
The short version
Section titled “The short version”bcrypt for legacy. argon2id for new code.
bcrypt was the right answer in 1999. It’s still an answer in 2026. But if you’re starting a new project, there is no reason to pick the 1999 answer when the 2015 answer exists, is OWASP-recommended, is built into your runtime, and costs you one line of configuration.
Dulak uses argon2id with OWASP-recommended parameters because the alternative is using a hashing algorithm designed before GPUs existed and pretending the last 15 years of hardware evolution didn’t happen.
See the auth and sessions guide for how password hashing fits into the full authentication flow — DB-backed sessions, CSRF protection, and route guards.