Back to articles index
Format comparisons

bcrypt vs Argon2 vs scrypt — which password-hashing KDF should you pick?

Why you must not store passwords with SHA-256 alone, and how bcrypt, Argon2, and scrypt compare across CPU cost, memory hardness, parallel-attack resistance, and library maturity.

Why SHA-256 is not a password hash

Before comparing KDFs, the elephant in the room: never store user passwords with a general-purpose hash like SHA-256 or SHA-512. These functions are designed to be fast, and that is exactly the wrong property for password storage. A single modern GPU computes more than ten billion SHA-256 hashes per second, which means an eight-character password — even when salted — can be cracked offline in hours to days using precomputed tables or brute force. The moment a database dump leaks, salted SHA-256 buys very little time.

That is why password storage uses a KDF (Key Derivation Function) such as bcrypt, scrypt, or Argon2. Each is a deliberately slow hash designed to throttle attackers. Four axes help you compare them. Cost tunability — can you turn the dial up as CPUs get faster? Memory-hardness — does the algorithm force the attacker to use a lot of RAM per guess, neutralising GPU and ASIC advantages? Parallelism resistance — can the attacker amortise verification cost cheaply? Maturity — are there audited language-standard implementations you can lean on?

Side-by-side comparison

PropertybcryptscryptArgon2id
Year199920092015
Base primitiveBlowfish-derivedPBKDF2 + Salsa20/8BLAKE2b
Cost knobcost factor (log work factor)N (CPU/memory)t_cost (iterations)
Memory-hardnessWeak (fixed 4 KB)Strong (N r 128 bytes)Strong (m_cost in MiB)
Parallelism paramNonep (parallelism)p (lanes)
Output length60 chars (fixed)ConfigurableConfigurable
Input limit72 bytesNoneNone
Standardisationde factoRFC 7914RFC 9106
Sensible defaultscost=12 (~250 ms)N=2^17, r=8, p=1m=64 MiB, t=3, p=4

bcrypt has been in production since 1999 and is the most battle-tested of the three. The catch is structural: cost factor scales CPU time but memory consumption is fixed at 4 KB, so the GPU gap has narrowed steadily. bcrypt also has the infamous 72-byte input ceiling — anything beyond that is silently truncated, so naive use with long passphrases drops bytes off the end. scrypt was the first practical memory-hard KDF and powers Bitcoin Core wallet encryption and Litecoin’s proof of work. Argon2 won the 2015 Password Hashing Competition and is the default recommendation for new code. Argon2id is the hybrid variant combining side-channel resistance (from Argon2i) and GPU resistance (from Argon2d); both OWASP and RFC 9106 recommend it. Note that PBKDF2 — though NIST-standardised in SP 800-132 — is not memory-hard, so even at 600,000 iterations it is generally considered weaker than bcrypt against a modern attacker.

Picking a KDF by use case

Existing systems already running bcrypt: do not rush to migrate. Keep the cost factor at 12 or higher and re-hash opportunistically on the next successful login. bcrypt has 25 years of audited implementations — bcrypt on npm, passlib for Python, golang.org/x/crypto/bcrypt for Go — and trading that maturity for a marginal security win is rarely the right call.

New password storage: use Argon2id. Start at m_cost=64 MiB, t_cost=3, p=4 and tune from there based on your server’s response budget. Reference implementations include argon2 on npm, argon2-cffi for Python, and the argon2 Rust crate.

Cryptocurrency wallets and master-key derivation: scrypt is still common (BIP38, Litecoin) and remains a perfectly valid choice for legacy compatibility; Argon2id is fine for greenfield specs. One important distinction — do not use a KDF for JWT signing keys or session tokens. Those need a cryptographic random source like crypto.randomBytes(32), not an intentionally slow hash. For general-purpose hashing (file integrity, content addressing) reach for hash-generate instead; mixing up the two roles wastes CPU without buying any security.

Reading a bcrypt string in the browser

The practical rule for application developers is “do not roll your own KDF”. Let mature libraries parse strings like $2a$12$... for you. Still, you sometimes hold a bcrypt hash you would rather not paste into a third-party analyser — a leaked DB dump, a Heroku environment variable, a customer support ticket. bcrypt-info decomposes the version prefix ($2a$ / $2b$ / $2y$), the cost factor, the salt, and the hash body entirely inside your browser. Production hashes never leave the page. The implementation is on GitHub, and a quick look at the DevTools Network tab confirms there is no outbound request.

One subtle gotcha: bcrypt’s $2y$ prefix is a PHP-flavoured tag using the same algorithm as $2a$, while $2b$ is the OpenBSD-corrected variant with stricter string-length handling. Application code that hard-codes “only accept $2a$” will silently reject perfectly valid hashes produced by Laravel or Symfony. Make sure your verifier handles all three prefixes.