If you're storing passwords, you're not encrypting them and you're definitely not hashing them with SHA-256. You're using a slow, salted, adaptive hash designed for exactly this job. Bcrypt is one of the few battle-tested options, and it's been holding up for over two decades.
Here's what it actually does, when to reach for it, and the gotchas that trip people up in real projects.
What bcrypt actually is
Bcrypt is a password hashing function based on the Blowfish cipher. Two things make it good for passwords:
- It's deliberately slow. A fast hash like SHA-256 is a liability here — an attacker with a GPU can try billions of guesses per second. Bcrypt has a tunable work factor (the "cost") so you can dial how expensive each hash is.
- It salts automatically. Every hash gets a random salt baked in, so two identical passwords produce different hashes. No separate salt column to manage.
A bcrypt hash looks like this:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUWThat single string encodes everything you need to verify a password later:
$2b$— the algorithm version12— the cost factor (2^12 = 4096 rounds)- the next 22 characters — the salt
- the rest — the actual hash
Because the salt and cost travel with the hash, verification is self-contained. You store one column and you're done.
When to use it
Reach for bcrypt when you're storing user passwords in a database and need to verify them at login. That's the classic case. It's a good default for most web apps and APIs.
When not to use it:
- Hashing arbitrary data or checksums. Use SHA-256 for that — it's meant to be fast.
- Encrypting data you need to read back. Hashing is one-way. If you need the original value, you want encryption, not hashing.
- Signing tokens or API keys. HMAC or a dedicated scheme fits better.
If you're starting a greenfield project and want the modern recommendation, Argon2 is arguably the stronger choice today. But bcrypt is everywhere, well-supported in every language, and perfectly fine. Don't rip out working bcrypt to chase a newer algorithm.
A worked example
Here's Node.js with the bcrypt package. The pattern is the same in every language: one call to hash, one call to compare.
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// On signup — hash the plaintext password
async function register(plainPassword) {
const hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);
// store `hash` in the database
return hash;
}
// On login — compare against the stored hash
async function login(plainPassword, storedHash) {
const match = await bcrypt.compare(plainPassword, storedHash);
return match; // true or false
}Notice there's no separate salt handling and no manual comparison of strings. bcrypt.compare pulls the salt and cost out of the stored hash, re-runs the algorithm, and does a constant-time comparison for you. Don't try to compare hashes with === yourself.
Want to see what a hash looks like for a given input without wiring up code? You can generate one in the browser with the Bcrypt Hash Generator — handy for testing verification logic or seeding a dev database.
Common gotchas
The 72-byte limit
Bcrypt silently truncates input after 72 bytes. That means these two passwords may hash to the same value if they share the first 72 bytes:
aaaaaaaa...(72 chars)...password1
aaaaaaaa...(72 chars)...password2Rarely a problem for human passwords, but it becomes real if you pre-hash long inputs or accept very long passphrases. A common fix is to run the password through SHA-256 first and pass the hex digest to bcrypt — but be aware that null bytes in the SHA-256 output can cause issues in some implementations, so base64-encode it to be safe.
Picking the cost factor
The cost is a tradeoff between security and login latency. Every increment doubles the work. A reasonable target is around 250ms per hash on your production hardware. Cost 12 is a sensible default in 2024; benchmark on your actual servers rather than trusting a number from a blog post.
console.time('bcrypt');
await bcrypt.hash('test-password', 12);
console.timeEnd('bcrypt'); // aim for ~200-300msUpgrading cost over time
Because the cost is stored in the hash, you can raise it later. On a successful login, check whether the stored hash used an old cost and, if so, re-hash the plaintext (which you have in hand at that moment) and update the row. This lets you strengthen hashes gradually as hardware gets faster, without forcing a password reset.
Don't hash on the client
Hashing a password in the browser and sending the hash to the server doesn't help — the hash simply becomes the new password. Always hash server-side, and always send passwords over TLS.
Watch out for timing leaks
When a user doesn't exist, some code skips the bcrypt call and returns immediately. That makes login timing observably different for valid vs invalid usernames, leaking which accounts exist. Run a dummy compare against a fixed hash even when the user isn't found, so the response time stays roughly constant.
Quick checklist
- Use bcrypt (or Argon2) for passwords — never plain SHA/MD5.
- Store the whole hash string; the salt and cost are inside it.
- Use the library's
comparefunction, not your own equality check. - Benchmark and set a cost factor targeting ~250ms.
- Be aware of the 72-byte truncation.
- Hash server-side, over TLS, and avoid timing side channels.
Bcrypt isn't glamorous, and that's the point. Get the basics right, pick a sane cost, and it'll quietly do its job for years.