← Back to blog
Security·July 10, 2026·8 min read

bcrypt vs scrypt vs Argon2: How Password Hashing Actually Differs

MD5 and SHA-256 are fast, and fast is exactly the wrong property for password storage. Here is how bcrypt, scrypt, and Argon2 slow attackers down differently — and how to actually pick between them.

Every few months a company discloses a breach and the postmortem includes a line like "passwords were hashed with MD5" or, worse, "passwords were stored in plaintext." The damage in those cases isn't really about which hash function was picked — it's that the wrong category of function was used at all. MD5 and SHA-256 are fast. That's the whole problem. A general-purpose cryptographic hash is designed to verify a file didn't get corrupted in transit, and "fast" is a feature there. For password storage, fast is a liability: it's exactly what lets an attacker with a stolen hash database try billions of guesses a second on commodity GPUs.

bcrypt, scrypt, and Argon2 exist to fix that by being deliberately, tunably slow — and by consuming memory in ways that resist the hardware attackers actually use. But "deliberately slow" isn't one thing. Each of the three slows down attackers differently, and picking between them (or configuring the one your framework already ships) requires knowing what's actually being traded off.

Why fast hashing breaks password storage

A raw SHA-256 hash runs in a few nanoseconds. A modern GPU can compute billions of SHA-256 hashes per second, and specialized ASICs push that further. If an attacker steals a database of SHA-256(password) values, they don't need to break the hash function — they just need to hash every entry in a wordlist (or every string up to some length) and compare. Add a per-user random salt and you stop precomputed rainbow-table attacks, but you do nothing to slow down a targeted brute-force against one hash, because salting doesn't change how fast the hash function itself runs.

Password hashing functions solve this with two knobs general-purpose hashes don't have:

  1. Deliberate computational cost — a tunable "cost factor" that makes each hash take milliseconds instead of nanoseconds, scaled to hardware that gets faster every year.
  2. Memory hardness — forcing the computation to touch a large, unpredictable block of memory, which is expensive to parallelize on GPUs and ASICs where memory bandwidth (not raw compute) is the scarce resource.

bcrypt only really has the first knob. scrypt and Argon2 have both.

bcrypt: the durable default

bcrypt dates to 1999 (Provos and Mazières, designed for OpenBSD) and is built on a modified version of the Blowfish cipher's key schedule, run through a deliberately slow, iterative setup phase called EksBlowfish. Its tuning parameter is a single cost factor, usually written as $2b$12$... where 12 means 2¹² = 4,096 rounds. Each increment of the cost factor doubles the work, so going from cost 10 to cost 12 is a 4x slowdown, not a 20% one.

bcrypt's real strength today isn't cryptographic elegance — it's survivorship. It has been battle-tested for 25+ years with no practical breaks, it's implemented correctly in essentially every language and framework, and its output format ($2b$<cost>$<salt><hash>, all self-contained in one 60-character string) makes storage and cost-factor migration trivial: you don't need a separate salt column, and you can detect "this hash was made with an outdated cost factor" just by parsing the string on login and re-hashing if needed.

Its real weakness is that it has no memory-hardness knob. A cost-12 bcrypt hash takes a fixed, small amount of memory to compute regardless of cost factor, which means an attacker can throw enormous numbers of parallel compute units — GPUs, FPGAs, ASICs — at cracking it, limited only by compute, not memory bandwidth. bcrypt also silently truncates input passwords at 72 bytes in the original specification, which matters if you're hashing something more than a passphrase (a common gotcha: hashing a long passphrase and getting collisions between two entirely different phrases that share the same first 72 bytes).

scrypt: memory-hard by design

scrypt (Percival, 2009, originally built for the Tarsnap backup service) was the first widely deployed answer to bcrypt's weak spot: it deliberately requires a large, configurable block of memory to compute, not just CPU cycles. The idea is that GPUs and ASICs are extremely good at parallel compute but comparatively bad at giving each parallel unit its own large chunk of fast memory — DRAM is expensive and shared bandwidth becomes the bottleneck. Force every hash attempt to need, say, 16 MB of working memory, and an attacker who could run a million bcrypt attempts in parallel on a GPU now can't fit a million 16 MB blocks in that GPU's memory at once.

scrypt exposes three parameters instead of bcrypt's one:

scrypt's downside is that it's harder to tune correctly than bcrypt's single number, and a subtler issue: at low N values it isn't meaningfully memory-hard, so a badly configured scrypt call can be weaker than a well-configured bcrypt call. It also predates the wave of side-channel research that led to Argon2's design, so it doesn't have Argon2's explicit resistance to certain timing and cache-based attacks.

Argon2: the current standard, and why it won a competition

Argon2 won the Password Hashing Competition in 2015, a multi-year, open, adversarial review process specifically meant to replace bcrypt and scrypt with something designed from scratch against modern attack hardware. It's now the recommendation in OWASP's password storage guidance, and it comes in three variants:

Argon2 exposes three independent cost parameters rather than scrypt's coupled ones:

ParameterWhat it controlsOWASP-suggested starting point (Argon2id)
Memory cost (m)KiB of RAM required per hash19 MiB (minimum) to 46 MiB+ depending on load tolerance
Time cost (t)Number of iterations over that memory2
Parallelism (p)Number of parallel lanes/threads1

Separating memory and time cost is the meaningful upgrade over scrypt: you can independently dial up memory hardness (to resist parallel hardware attacks) without also multiplying wall-clock latency, or vice versa, which makes it much easier to hit a specific "acceptable login latency, maximum attacker cost" target than juggling scrypt's N/r/p interaction.

Side by side

bcryptscryptArgon2id
Released199920092015 (PHC winner)
Memory-hardNoYesYes (independently tunable)
Tuning parameters1 (cost factor)3 (N, r, p)3 (memory, time, parallelism)
Max password input72 bytes (silent truncation)Effectively unboundedEffectively unbounded
Side-channel resistanceN/A (not memory-hard)Weaker, no explicit design goalExplicit (Argon2id hybrid design)
Ecosystem maturityExtremely mature, in nearly every language/frameworkMature, native in OpenSSL 1.1+, Node cryptoMature, but slightly younger tooling in some ecosystems
OWASP current recommendationAcceptable if Argon2 unavailableAcceptable alternativePreferred default
Best fitLegacy systems, environments where Argon2 libraries aren't available, compliance regimes that specifically require itSystems already standardized on it (many did before Argon2 existed), general-purpose KDF use beyond just passwordsNew systems with no legacy constraint

The table doesn't capture the operational reality, though: switching an existing production system's password hash function is not a config change. You can't re-hash a password you don't have — you only have the old hash. The standard migration pattern is to hash new and reset passwords with the new algorithm going forward, and re-hash existing users' passwords with the new algorithm at their next successful login (when you briefly have the plaintext password in hand to verify against the old hash and re-hash with the new one). That means a full migration takes as long as it takes inactive users to log back in, which in practice means some fraction of stored hashes stay on the old algorithm indefinitely unless you force a password reset.

Picking one in practice

If you're starting a new system today: use Argon2id, and start from OWASP's current baseline parameters rather than guessing — the guidance gets revised as hardware gets faster, so check it at build time rather than trusting a number from a five-year-old blog post. If your framework or language ecosystem only has mature, well-audited bcrypt support and Argon2 support is thin or unmaintained, bcrypt is still a legitimate choice — a well-implemented bcrypt beats a poorly-implemented or unmaintained Argon2 library. scrypt is reasonable if you're already standardized on it, or if you need a general-purpose memory-hard KDF for something beyond password hashing (scrypt is also used for key derivation in disk encryption and some cryptocurrency wallets), but for new password-hashing work specifically, there's little reason to pick it over Argon2id today.

Whichever you pick, the parameter tuning matters more than the algorithm choice within a reasonable range: a bcrypt cost factor of 4 is weaker than a well-tuned Argon2id call, and an Argon2id call with memory cost set too low loses most of its advantage over bcrypt. The right approach is to benchmark on your actual production hardware, target roughly 200-500ms of hashing time per login attempt (slow enough to matter to an attacker doing billions of attempts, fast enough not to visibly delay a real user), and re-check that target periodically as your infrastructure or the attacker landscape changes. If you want to see the shape of bcrypt or scrypt output without wiring up a library, Utilix's bcrypt hash and verify tool and scrypt hash tool run both locally in the browser, which is a fast way to sanity-check what a given cost factor actually produces before you commit to it in code.

#password-hashing#bcrypt#argon2#scrypt#security#cryptography