Why bcrypt and not MD5: the cracking-cost math

Updated 2026-08-09

A single RTX 4090 computes 164.1 billion MD5 hashes per second. The same card manages about 1,437 bcrypt hashes per second at cost factor 12. That ratio — roughly 114 million to one — is the entire argument for bcrypt, and it is worth working through the arithmetic once so the advice stops being folklore and starts being a number you can defend in a design review.

The numbers below come from the widely cited hashcat v6.2.6 benchmark of the RTX 4090 published by Chick3nman (a hashcat team member); the arithmetic below is worked from that figure rather than quoted secondhand. This matters because most password-storage advice online quotes stale or invented speeds.

One consumer GPU, five algorithms

Hashcat's benchmark mode measures raw guessing throughput against a single stored hash. On an RTX 4090 — a card anyone can buy at retail — the results for common password hash choices look like this:

Algorithmhashcat modeRTX 4090 speedDesigned for passwords?
NTLM1000288.5 GH/sNo (Windows legacy)
MD50164.1 GH/sNo
SHA-110050.6 GH/sNo
SHA-256140022.0 GH/sNo
bcrypt, cost 53200184.0 kH/sYes

Two things stand out. First, SHA-256 is not a fix: it is only about 7.5x slower than MD5 for an attacker, which buys you nothing meaningful. MD5, SHA-1, and SHA-256 were all designed to be fast — that is a feature for file checksums and a catastrophe for password storage. Second, the bcrypt number is measured at cost 5, the artificial minimum used for benchmarking. Every increment of the cost factor doubles the work, so cost 12 is 2^7 = 128x slower than the benchmark figure: about 1,437 hashes per second.

Exhausting eight lowercase characters: 1.3 seconds vs 4.6 years

Take the weakest password policy still seen in the wild: exactly 8 lowercase letters. The keyspace is 26^8 = 208,827,064,576 candidates — about 37.6 bits of entropy. Divide by the guessing rate:

python3
keyspace = 26**8            # 208_827_064_576

md5_rate  = 164.1e9         # H/s, RTX 4090
bcrypt_12 = 184.0e3 / 2**7  # cost 5 benchmark scaled to cost 12 = 1437.5 H/s

keyspace / md5_rate         # 1.27 seconds
keyspace / bcrypt_12        # 145,270,000 s = 1,681 days = 4.6 years

The same password survives 1.3 seconds behind MD5 and 4.6 years behind bcrypt cost 12 — on identical hardware, against a single hash. Widen the character set and the gap becomes absurd: all 95 printable ASCII characters at length 8 gives 95^8 = 6.6 x 10^15 candidates, which MD5 exhausts in about 11 hours while bcrypt cost 12 needs roughly 146,000 years. A password that is trivially crackable under MD5 is effectively permanent under bcrypt.

One caveat: real attackers rarely brute-force blindly. They run dictionaries, leaked-password lists, and mangling rules first, so a human-chosen password like Password123! dies quickly under any algorithm. The slow hash buys time; only randomness buys safety. A 16-character password from our password generator (75-character set: letters, digits, and 13 symbols) carries about 99.7 bits of entropy, which is beyond exhaustive search under any algorithm.

This is also where a strength meter will mislead you, ours included. The password strength checker on this site is a checklist (length, character classes, no repeated runs), not an entropy or dictionary analysis. Password123! passes all seven of its checks and rates Strong, yet it would fall to any rules-based dictionary attack in seconds. Treat a checklist score as a floor, not a verdict, and use the password generator — which draws from crypto.getRandomValues with rejection sampling to avoid modulo bias — when you actually need a strong secret.

Reading a bcrypt hash, field by field

Hash the same input twice with bcrypt and you get two different outputs. Both verify. Here is the same password hashed twice at cost 12:

bcrypt output
$2b$12$YfEYH3HKu7nDyiB6Cw0fmutbgIHRY25KXwXTvntD9QOrlMDkoSyHa
$2b$12$CaPMTRlBsdSD.JBtIBlQU.1Ib5jsdereJWGurMTPnxzCDtJFDhs5i

$2b$  12$  <22 chars of salt>              <31 chars of hash>
 |     |   YfEYH3HKu7nDyiB6Cw0fmu          tbgIHRY25KXwXTvntD9QOrlMDkoSyHa
 |     cost factor (2^12 iterations)
 version identifier

The 128-bit salt is generated fresh per hash and stored inside the string, which is why no separate salt column is needed and why verification works: bcrypt.compare() re-reads the salt and cost from the stored hash and re-derives. The salt is what kills rainbow tables — a precomputed table would need a separate entry per salt value, making precomputation pointless. Note that salting alone does not rescue MD5: salted MD5 still runs at GPU speed per guess. Salts defeat precomputation; only the cost factor defeats throughput.

Picking a cost factor: measure, don't guess

The right cost factor is the highest one your login latency budget tolerates. Measured with bcryptjs (pure JavaScript, the same library our bcrypt tool runs in your browser) in Node on a current laptop:

Cost factorIterationsTime to hash (this machine)RTX 4090 attack rate
102^1057 ms~5,750 H/s
122^12204 ms~1,437 H/s
142^14809 ms~359 H/s

Cost 10 was the common default a decade ago; against a 4090 it exhausts the 26^8 space in about 420 days versus 4.6 years at cost 12. For interactive logins in 2026, cost 12 is a sensible floor. Re-benchmark on your production hardware, not your laptop, and remember hashing happens per login attempt — a credential-stuffing wave at cost 14 can become an accidental self-DoS, which is exactly the trade the cost factor is supposed to make you think about.

The same latency is visible in the bcrypt tool on this site, which runs bcryptjs in the page across cost factors 8 through 14 and has a verify tab that checks a plaintext against an existing hash — handy for confirming a hash in a database dump is what you think it is. Being pure JavaScript, it is slower than native bindings, so cost 14 takes a noticeable moment; that is the algorithm working as designed, not a bug. The MD5 and SHA-256 tools here are for checksums and cache keys, never for passwords.

One trap worth knowing: bcrypt silently truncates input at 72 bytes. We verified this directly — a hash of 72 'a' characters successfully verifies against 73 and even 80 'a' characters. If you allow very long passphrases, pre-hash with SHA-256 (then base64 the digest to avoid NUL bytes) before bcrypt, or use Argon2, which has no such limit.

When Argon2 instead

bcrypt's weakness is that it is CPU-bound but memory-light, so GPUs and FPGAs still parallelize it reasonably well — the 184 kH/s benchmark figure exists precisely because thousands of GPU cores each run their own instance. Argon2, winner of the Password Hashing Competition in 2015, adds a tunable memory parameter: forcing, say, 64 MiB per hash means an attacker's GPU can only run as many parallel instances as its VRAM allows. For new systems, Argon2id is the better default and is what OWASP's password storage guidance recommends first. bcrypt remains a perfectly defensible choice for existing systems — being 25+ years old with no practical breaks is a feature — and migrating is straightforward: rehash each user's password at their next successful login.

What you should never do is use MD5 or SHA-256 for passwords, salted or not. MD5's collision resistance has been broken since Wang et al.'s 2004 attack (RFC 6151 summarizes the damage), but for password storage the collision attacks are almost beside the point — raw speed is the disqualifier, and that applies to the whole fast-hash family.

Questions people ask

Is MD5 ever acceptable to use?

For non-security purposes, yes: cache keys, content deduplication, ETag generation, detecting accidental corruption. It is unacceptable anywhere an adversary is involved — passwords, signatures, certificate digests — because collisions are practical and guessing throughput is enormous.

Isn't SHA-256 with a salt good enough for passwords?

No. A salt defeats precomputed rainbow tables but does nothing about throughput: a single RTX 4090 still makes about 22 billion salted-SHA-256 guesses per second. You need an algorithm with a tunable work factor — bcrypt, scrypt, Argon2, or PBKDF2 with a high iteration count.

What bcrypt cost factor should I use in 2026?

Benchmark on your production hardware and pick the highest cost that keeps hashing under roughly 250 ms per login. On current server CPUs that typically lands at cost 12, sometimes 13. Cost 10 is now a legacy floor, not a recommendation.

Does adding a pepper help?

A pepper — a server-side secret mixed into every hash but stored outside the database — protects against the specific scenario where the database leaks but application config does not. It is worthwhile defense in depth (commonly HMAC the password with the pepper before bcrypt), but it does not replace a slow hash.

Try it yourself