How Bcrypt Hashes Passwords (And Why Cost Factor Matters)

How Bcrypt Hashes Passwords (And Why Cost Factor Matters)

A breach hits the news. Hackers dumped a database with millions of user records, and within hours the original passwords are floating around on credential-stuffing forums. Then a different breach happens, and somehow the leaked hashes are still resisting cracking attempts months later. The difference is almost always the password hashing function. The first site stored passwords with MD5 or SHA-256. The second used bcrypt with a sensible cost factor.

Bcrypt has been around since 1999 and remains one of the few password hashing functions that ages gracefully. Hardware gets faster, but bcrypt has a built-in dial that lets you turn up the cost so it stays slow regardless. Here is what the algorithm actually does, why the cost factor is the entire point, and how to pick a value that will still hold up in five years.

Why You Cannot Just Use SHA-256

The temptation is obvious. SHA-256 produces a fixed-length output, looks random, and is built into every standard library. Why not hash the password and store the digest?

Because SHA-256 is fast. A modern GPU can compute billions of SHA-256 hashes per second. If an attacker steals your database and finds 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8, they can brute-force every English word, every common password, every leaked password from prior breaches, in minutes. Salting helps against rainbow tables but does nothing about raw throughput.

Password hashing functions like bcrypt, scrypt, and Argon2 solve a different problem. They are intentionally slow, and you can make them slower as hardware improves. A bcrypt hash with cost factor 12 takes about 250 milliseconds to compute on a typical server. That is fine for a login endpoint that runs once per user session. It is catastrophic for an attacker who needs to try a billion guesses.

If you are still using a fast hash for passwords, even with salt, the hash generator tool will show you how trivially fast SHA-256 and SHA-512 actually are. Then compare to bcrypt and the difference becomes obvious.

Salt First, Always

Bcrypt generates a random 128-bit salt and combines it with the password before hashing. Two users with the same password get completely different hashes because their salts differ.

User A: password = "hunter2", salt = a8f3c1...
User B: password = "hunter2", salt = 9b2e7d...
Hash A: $2b$12$a8f3c1...XyZ
Hash B: $2b$12$9b2e7d...PqR

This kills two attack classes immediately. Rainbow tables (precomputed hash lookups) become useless because the attacker would need a separate table for every possible salt. Mass cracking is also defeated because the attacker has to guess each user's password individually rather than testing one guess against the entire user base at once.

The salt is stored alongside the hash in the bcrypt output string, not separately. When you verify a password, the implementation extracts the salt from the stored hash, applies it to the candidate password, and compares the result. You never need to manage salts as separate database columns.

What the Hash String Actually Means

A bcrypt hash looks like this:

$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

Each segment encodes information:

$2b$        algorithm version (2b is current)
$12$        cost factor (rounds = 2^12 = 4096)
N9qo8uLOickgx2ZMRZoMye   22-character base64 salt (16 bytes)
IjZAgcfl7p92ldGxad68LJZdL17lhWy   31-character base64 hash (24 bytes)

The version prefix matters. $2a$ was the original. $2b$ fixed a wraparound bug for very long passwords. $2y$ is a PHP-specific variant. $2x$ was a workaround for the old bug and should never be generated for new hashes. Modern libraries default to $2b$, which is what you want.

You can paste any bcrypt hash into the bcrypt generator and verifier to break it down and check verification against a candidate password. It is useful for debugging when a login is failing and you want to confirm whether the stored hash matches what your library is computing.

The Blowfish Connection

Bcrypt is built on top of the Blowfish cipher, designed by Bruce Schneier in 1993. The original 1999 paper by Niels Provos and David Mazieres at the USENIX conference introduced the construction now known as the Eksblowfish key setup.

Standard Blowfish initializes its internal state (P-array and S-boxes) once with a fixed setup phase. Bcrypt modifies this in two ways. First, it incorporates both the password and the salt into the key schedule. Second, and more importantly, it repeats the expensive key setup phase 2^cost times. Cost factor 12 means 4,096 rounds of key expansion before any actual encryption happens.

This is the part that makes bcrypt slow on purpose. The Eksblowfish key setup is memory-bound and resists GPU optimization in a way that simple iterated SHA-256 does not. A GPU has many cores but limited per-core memory bandwidth, and bcrypt's 4 KB internal state across many parallel rounds chokes that bandwidth.

After the expensive setup, bcrypt encrypts the constant string "OrpheanBeholderScryDoubt" (a 192-bit value) repeatedly using the derived key. The final ciphertext is the 24-byte hash you see in the output string. The Wikipedia bcrypt article has a clean walkthrough of the inner loop if you want the exact pseudocode.

Cost Factor Is the Entire Point

The cost factor (also called work factor or rounds) is a logarithmic dial. Each increment doubles the computation time. Cost 10 is twice as slow as cost 9. Cost 14 is sixteen times slower than cost 10.

Approximate timings on a 2024-era server CPU:

cost 8   ~15 ms
cost 10  ~60 ms
cost 12  ~250 ms
cost 13  ~500 ms
cost 14  ~1 second
cost 15  ~2 seconds

Picking a value is a tradeoff. Higher cost means more attacker work per guess but also slower legitimate logins. The widely cited rule of thumb is to target around 250-500 ms per hash on your production hardware. Most current guidance from the OWASP password storage cheat sheet recommends cost 10 as a minimum and 12 as a more comfortable default for 2026.

The crucial detail is that you can rehash on login. When a user signs in, their plaintext password is briefly available in memory. Check the cost factor on the stored hash, and if it is below your current target, recompute the hash at the new cost and update the database. Over a few months of user logins, your entire database upgrades itself.

const stored = await db.getPasswordHash(userId);
const valid = await bcrypt.compare(password, stored);
if (valid) {
  const currentCost = parseInt(stored.split('$')[2], 10);
  if (currentCost < TARGET_COST) {
    const upgraded = await bcrypt.hash(password, TARGET_COST);
    await db.updatePasswordHash(userId, upgraded);
  }
  return generateSession(userId);
}

The 72-Byte Truncation Trap

Bcrypt has a hard limit: only the first 72 bytes of the input password are used. Anything beyond that is silently ignored. This trips up two scenarios.

First, very long passphrases. If a user enters a 100-character diceware phrase generated by a diceware passphrase generator, only the first 72 bytes contribute to the hash. The remaining characters are dropped. The user thinks they have a 100-character secret; bcrypt is using maybe 60-65 of them depending on UTF-8 encoding.

Second, pre-hashing. Some systems try to extend bcrypt by hashing the password with SHA-256 first, then bcrypting the digest. This is fine if the SHA-256 output is hex-encoded (64 ASCII bytes, all under the limit). It backfires badly if the raw 32-byte digest is used and it contains a null byte, because some bcrypt implementations truncate at the first null. Auth0's hashing in action walkthrough covers this gotcha and a few related implementation pitfalls.

The simple fix is to enforce a reasonable maximum password length in your registration flow (say, 64 characters) and reject anything longer. Or migrate to Argon2id, which has no truncation limit. For estimating how much entropy users are actually contributing in their passwords, the password entropy calculator gives you a quick sanity check before any hashing happens.

When Bcrypt Is Not the Right Choice

Bcrypt is a password hashing function. It is wrong for almost everything else.

Do not use bcrypt for API token hashing. Tokens are already high-entropy random strings; you do not need a slow KDF. SHA-256 is correct here, and it is fast enough that token lookup does not become a bottleneck. The hash generator covers the standard digest functions when you need raw cryptographic hashing rather than password hashing.

Do not use bcrypt for HTTP basic auth files. Apache uses its own format for .htpasswd files, which supports bcrypt as one of several algorithms but with a specific encoding. The htpasswd generator produces the correct format directly.

Do not use bcrypt for general key derivation when you need long output. Bcrypt produces 24 bytes. If you need a 256-bit key for AES, that is fine. If you need 64 bytes for HMAC-SHA512, you need to either use Argon2id with the desired output length or chain bcrypt output through HKDF.

Do not use bcrypt as a generic message authentication function or content hash. It is parameterized, salted, and intentionally slow. Use HMAC for authentication and SHA-256 for content addressing.

Practical Defaults for 2026

If you are starting a new project today, the choice is between bcrypt and Argon2id. Argon2id is the OWASP-recommended winner of the Password Hashing Competition (2015) and resists GPU and ASIC attacks better than bcrypt due to its memory-hard design. Bcrypt is fine and still considered safe, with broader library support across older runtimes.

Reasonable defaults for bcrypt on a 2026 web app:

  • Cost factor 12 minimum, 13 if your login latency budget allows
  • Enforce a 64-character maximum password length to dodge the 72-byte trap
  • Rehash on successful login when the stored cost is below your current target
  • Use the $2b$ algorithm version, never $2a$ or $2x$ for new hashes
  • Never log, cache, or transmit raw passwords; only the bcrypt output goes to disk

For a quick check that your implementation is producing valid hashes and verifying correctly, the bcrypt generator and verifier lets you generate hashes at any cost factor and confirm round-trip verification before you ship the code to production. It also displays the parsed components of any bcrypt string so you can audit existing hashes in your database for outdated cost factors that should be upgraded on next login.

FAQ

What cost factor should I use for bcrypt in 2026?

12 is the comfortable default; 13 if you can afford the latency. Anything below 10 is too cheap for modern GPUs; cost 14+ adds login latency without much marginal security gain. The right move is to measure on your actual production hardware and target 250-500ms per hash, then bump the cost every couple of years as CPUs get faster.

Why does bcrypt only use the first 72 bytes of my password?

It's a quirk of the underlying Eksblowfish key schedule, which can only absorb a limited amount of key material. For ASCII passwords this means 72 characters; for UTF-8 passphrases with multi-byte characters, it can be fewer characters. The fix is either enforcing a max length in your registration form (say 64 chars) or pre-hashing the password through HMAC-SHA256 first.

Should I switch from bcrypt to Argon2id?

For greenfield projects, Argon2id is the OWASP-preferred answer in 2026 — it's memory-hard, GPU-resistant, has no 72-byte truncation, and won the Password Hashing Competition. For existing bcrypt deployments, don't migrate just to migrate; bcrypt is still considered secure. Migrate gradually by rehashing on next login if you decide to switch.

Does the salt need to be stored separately?

No, and that's one of bcrypt's nicer properties. The 22-character base64 salt is embedded in the hash string between the cost factor and the digest. When you call bcrypt.compare(password, storedHash), the library extracts the salt from the stored hash automatically. You only need one database column for the whole hash.

Can I just SHA-256 the password before bcrypt to bypass the 72-byte limit?

You can, but watch out — if you use the raw 32-byte SHA-256 binary digest, some bcrypt implementations truncate at null bytes. Use a hex-encoded (64 ASCII chars) or base64-encoded (44 chars) digest, or use HMAC-SHA256 with a server-side pepper. Or just switch to Argon2id, which has no truncation at all.

Is `$2a$` or `$2b$` better?

Use $2b$ for new hashes. $2a$ had a wraparound bug for passwords near the 72-byte boundary that was fixed in $2b$. $2y$ is a PHP-specific variant. $2x$ was a workaround for the $2a$ bug and should never be generated — only verified for legacy hashes. Modern libraries default to $2b$.

How do I upgrade existing bcrypt hashes to a higher cost?

Rehash on successful login. When a user signs in correctly, you have their plaintext password in memory briefly — check the cost factor of the stored hash, and if it's below your current target, recompute the hash at the new cost and update the database. Inactive users keep their old cost (which is still secure when first written) until they log in.

Is bcrypt safe against quantum computers?

Quantum computers don't really threaten password hashes the way they threaten asymmetric crypto. Grover's algorithm can speed up brute force, but bcrypt's per-attempt cost still applies — quantum or classical, you still need 250ms per guess at cost 12. The doomsday quantum scenario for crypto is RSA and ECC, not bcrypt or Argon2.