If you're storing passwords in 2024, bcrypt is still one of the right answers. It's been around since 1999, it's battle-tested, and it's available in every language you're likely to use. But plenty of developers reach for it without really understanding what it does — and that leads to subtle bugs and false confidence.
What bcrypt actually is
Bcrypt is a password-hashing function based on the Blowfish cipher. Two things make it suitable for passwords, where a plain hash like SHA-256 is not:
- It's deliberately slow. Bcrypt has a configurable work factor (also called cost). Higher cost means more rounds of computation, which makes brute-force attacks expensive.
- It salts automatically. Every hash includes a randomly generated salt, so two identical passwords produce different hashes. You don't manage the salt yourself — it's baked into the output.
A bcrypt hash looks like this:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUWThat string is self-describing. $2b$ is the algorithm version, 12 is the cost factor, the next 22 characters are the salt, and the rest is the actual hash. Because everything you need to verify a password is contained in that one string, you store just this in your database — nothing else.
Why you'd use it (and when you wouldn't)
Use bcrypt whenever you need to store user passwords and later check them at login. That's the classic case. The slowness that makes it a bad choice for general-purpose hashing is exactly what makes it good here — a password only needs to be verified occasionally, so a few hundred milliseconds is fine, while an attacker who steals your database can only try a handful of guesses per second per core.
Don't use bcrypt for:
- Hashing files or generating checksums — use SHA-256 for that.
- Deriving encryption keys from a passphrase — Argon2id or scrypt are better suited, though bcrypt-derived approaches exist.
- Anything performance-sensitive where you hash on every request. That's a design smell.
If you're starting a greenfield project today, Argon2id is the modern recommendation from the password-hashing competition. But bcrypt remains perfectly acceptable, and its ubiquity means you'll rarely fight tooling issues.
The gotchas that bite people
1. The 72-byte truncation limit
Bcrypt only looks at the first 72 bytes of the input. Anything beyond that is silently ignored. For most passwords this never matters, but if you pre-hash long inputs or accept very long passphrases, two different long passwords can produce the same hash. A common mitigation is to SHA-256 the password first and base64-encode it before feeding it to bcrypt — but do that consistently or you'll break existing logins.
2. Picking the wrong cost factor
The cost is exponential: cost 12 does twice the work of cost 11. A good target is roughly 250–500ms per hash on your production hardware. Benchmark it. A cost of 10 was reasonable a decade ago; 12 is a sensible default now. Don't crank it to 15 without measuring — you can accidentally DoS your own login endpoint.
3. Comparing hashes yourself
Never do storedHash === computedHash. Use the library's compare function, which handles the salt extraction and uses a constant-time comparison to avoid timing attacks.
4. Null bytes
Some older bcrypt implementations truncate at the first null byte. It's rare, but worth knowing if you're handling binary or unusual input.
A short worked example
Here's the standard pattern in Node.js using the bcrypt package. Hashing on signup, verifying on login:
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// On signup
async function hashPassword(plain) {
return bcrypt.hash(plain, SALT_ROUNDS);
}
// On login
async function verifyPassword(plain, storedHash) {
return bcrypt.compare(plain, storedHash);
}
// Usage
const hash = await hashPassword('correct horse battery staple');
// store `hash` in the database as a string
const ok = await verifyPassword('correct horse battery staple', hash);
console.log(ok); // trueNotice you never touch the salt directly, and there's no separate salt column. The whole verification flow is two function calls.
The equivalent in Python with passlib is just as tidy:
from passlib.hash import bcrypt
hashed = bcrypt.using(rounds=12).hash("correct horse battery staple")
bcrypt.verify("correct horse battery staple", hashed) # TrueUpgrading the cost factor over time
Hardware gets faster, so today's cost factor will be too weak in five years. The neat trick: when a user logs in successfully, check whether their stored hash uses an outdated cost. If it does, re-hash the plaintext (which you have in hand at that moment) with the new cost and save it. Over time your database migrates itself with no forced resets. Most libraries expose a needsRehash helper for exactly this.
Inspecting and testing hashes quickly
When debugging authentication, it's handy to generate a bcrypt hash on the fly to compare behaviours, or to seed a test database with a known password. Rather than writing a throwaway script every time, you can use an online Bcrypt Hash Generator to produce a hash with a chosen cost factor and eyeball its structure. It's useful for confirming that your library's output matches expectations and for spotting when a cost factor is set wrong.
The short version
Bcrypt does three things you care about: it salts automatically, it's tunably slow, and it stores everything it needs in one self-contained string. Use a cost factor around 12, always compare with the library's built-in function, remember the 72-byte limit, and re-hash on login as costs age. Get those right and you've handled password storage responsibly — which, given how often it's done badly, puts you ahead of a lot of production code out there.