Last year I was doing a security audit on a legacy codebase and found a users table with passwords stored in plain SHA-256. No salt. No iteration. Just a straight hash. I ran a rainbow table against it and cracked 60% of the passwords in under four minutes. That project became the reason I started thinking hard about password hashing and why bcrypt still matters in 2026.
The Problem with Fast Hashes
SHA-256, MD5, SHA-1 -- these are all designed to be fast. That is their job. When you are verifying file integrity or building a Merkle tree, speed is a feature. When you are hashing passwords, speed is a vulnerability.
A modern GPU can compute billions of SHA-256 hashes per second. That means an attacker with a decent graphics card can brute-force an eight-character password in hours, not years. The math is brutal: if your hash function is fast, your passwords are weak, regardless of what your users type in.
What Makes Bcrypt Different
Bcrypt was designed by Niels Provos and David Mazieres in 1999, based on the Blowfish cipher. It introduced two ideas that changed password security forever:
1. A built-in salt. Every bcrypt hash includes a 128-bit random salt, generated automatically. Two users with the password "hunter2" will have completely different hashes. Rainbow tables become useless.
2. A configurable cost factor. The cost factor (also called work factor or rounds) controls how many iterations the algorithm runs. A cost of 10 means 2^10 = 1,024 iterations. A cost of 12 means 4,096 iterations. As hardware gets faster, you increase the cost factor and the hash stays expensive.
Here is what a bcrypt hash looks like:
$2b$12$LJ3m4ys3Lg2VBe7Fz1eOa.kXxTbiKGwOlWR0.ztONLASqRqBfK4i6
Breaking that down:
-
$2b$-- the algorithm version -
12$-- the cost factor (2^12 rounds) - The next 22 characters -- the salt (base64 encoded)
- The remaining characters -- the actual hash
Everything you need to verify the password is embedded in the string itself. No separate salt column. No configuration lookup.
Choosing the Right Cost Factor
This is where most developers either overthink or underthink things. The goal is simple: make the hash take about 250ms to compute on your server hardware. That is fast enough that users will not notice a delay during login but slow enough that an attacker hashing millions of guesses will be waiting for years.
Here is how I benchmark it:
const bcrypt = require('bcrypt');
async function findCost() {
for (let cost = 8; cost <= 16; cost++) {
const start = Date.now();
await bcrypt.hash('testpassword', cost);
const duration = Date.now() - start;
console.log(`Cost ${cost}: ${duration}ms`);
}
}
findCost();
On my M2 MacBook Pro, cost 12 takes about 230ms. On a production server with less powerful CPUs, cost 10 or 11 might be the sweet spot. Run this on your actual deployment hardware and pick accordingly.
Five Mistakes I See Constantly
1. Using cost factor 10 because the tutorial said so. Cost 10 was reasonable in 2015. Hardware has gotten faster. Benchmark your own server and adjust.
2. Generating the salt separately. Bcrypt generates its own salt. When you call bcrypt.hash(password, rounds), the salt is created internally. Passing your own salt is possible but unnecessary and error-prone.
3. Comparing hashes with ===. Never do string comparison on hashes directly. Use bcrypt.compare(). It handles timing-safe comparison internally, preventing timing attacks where an attacker measures response time to guess characters.
// Wrong
if (storedHash === bcrypt.hashSync(inputPassword, storedHash)) { ... }
// Right
const match = await bcrypt.compare(inputPassword, storedHash);
4. Not upgrading hashes on login. When you increase your cost factor, existing hashes still use the old cost. On successful login, re-hash the password with the new cost factor and update the database. This is called progressive rehashing and it is trivially easy to implement.
5. Truncating passwords before hashing. Bcrypt has a 72-byte input limit. If your users have very long passwords, consider pre-hashing with SHA-256 before passing to bcrypt. But do not silently truncate -- either document the limit or handle it.
When to Use Something Else
Bcrypt is battle-tested and reliable, but it is not the only option. Argon2 won the Password Hashing Competition in 2015 and offers memory-hard hashing, which makes GPU attacks significantly more expensive. Scrypt is another solid choice with tunable memory requirements.
That said, bcrypt is supported everywhere, understood by every security auditor, and has decades of real-world deployment. For most applications, it remains the pragmatic choice.
A Practical Workflow
When I need to quickly generate bcrypt hashes for seeding a database, testing authentication flows, or verifying that my implementation matches expected output, I use the bcrypt generator I built at zovo.one. It lets you pick the cost factor, paste a password, and get a hash instantly -- useful for development without writing a throwaway script every time.
I am Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.
Top comments (0)