If you're storing passwords, you're not storing passwords — you're storing hashes. Bcrypt has been the go-to algorithm for that job for a long time, and for good reason. It's slow on purpose, it salts automatically, and it's been battle-tested since 1999. Let's talk about how to actually use it without shooting yourself in the foot.
What bcrypt actually is
Bcrypt is a password-hashing function built on the Blowfish cipher. Unlike general-purpose hashes like SHA-256, it's deliberately designed to be expensive to compute. That's the whole point. A fast hash is great for checksums and terrible for passwords, because an attacker who steals your database can run billions of SHA-256 guesses per second on a GPU.
Bcrypt fights that with a tunable work factor (also called the cost). Bump the cost up and each hash takes longer to compute — for you and for the attacker. As hardware gets faster, you raise the cost. That adaptability is what keeps bcrypt relevant decades later.
It also handles salting for you. Every hash includes a random salt baked into the output string, so two users with the same password get completely different hashes. No rainbow tables, no manual salt column to manage.
Reading a bcrypt hash
A bcrypt hash looks like this:
$2b$12$Nt6Fp0k9zQ8QvY.7yXjqu.4pQ0dJ3mF9sK2nH1cV8wL5rT6bU9xWeEach part means something:
$2b$— the algorithm version identifier.12— the cost factor (2^12 = 4096 rounds).- The next 22 characters — the salt.
- The rest — the actual hash.
Because the salt and cost live inside the string, verification needs nothing but the stored hash and the password the user typed. No separate columns.
When and why you'd use it
Reach for bcrypt whenever you're storing user passwords and don't have a strong reason to use something else. It's supported everywhere, the libraries are mature, and it's hard to misuse in a catastrophic way.
That said, bcrypt isn't the only modern option. Argon2 won the Password Hashing Competition in 2015 and is memory-hard, which makes it more resistant to GPU and ASIC attacks. scrypt is another memory-hard choice. If you're starting a greenfield project and your platform has a solid Argon2 library, it's a great pick. But bcrypt remains a perfectly defensible, well-understood default — and if you inherit a codebase already using it, there's usually no urgent reason to migrate.
One thing bcrypt is not for: hashing arbitrary data, session tokens, or anything that needs to be fast. Use it strictly for secrets that humans choose.
Common gotchas
The 72-byte truncation limit
Bcrypt silently ignores everything past the first 72 bytes of input. Most people never hit this — but if you let users paste in very long passphrases, or if you pre-hash passwords, you can run into it. A common footgun is pre-hashing with something like SHA-256 to work around the limit, then base64-encoding the result. If you're not careful about encoding, you can end up feeding null bytes that truncate early. If you need to support long inputs, hash to raw bytes and base64 before passing to bcrypt, and test it.
Picking the right cost
A cost that's too low is insecure; too high and your login endpoint becomes a DoS vector against yourself. The rule of thumb: tune the cost so a single hash takes somewhere around 250–500ms on your production hardware. Benchmark it on the box that'll actually run it, not your laptop. As of the mid-2020s, a cost of 12 is a reasonable starting point, but measure rather than cargo-cult.
Comparing hashes correctly
Never compare hashes with a plain string equality operator in a way that leaks timing. Use the library's built-in compare / verify function, which does a constant-time comparison and re-derives the hash from the stored salt and cost for you.
Don't roll your own salt
Bcrypt generates the salt. If you find yourself manually creating salts or concatenating them, you're using the API wrong. Let the library do it.
A short worked example
Here's the typical flow in Node.js using the bcrypt package. Registration hashes the password; login verifies it.
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// On sign-up
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');
console.log(hash);
// $2b$12$....
const ok = await verifyPassword('correct horse battery staple', hash);
console.log(ok); // trueNotice you never store or manage the salt yourself, and verification takes only the plaintext and the stored hash. That's the whole API surface you need for 95% of cases.
The Python side looks almost identical with the bcrypt library:
import bcrypt
password = b"correct horse battery staple"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
if bcrypt.checkpw(password, hashed):
print("match")If you just want to eyeball what a bcrypt hash looks like, experiment with different cost factors, or verify a hash by hand while debugging, our Bcrypt Hash Generator does it in the browser — handy when you're checking a migration or writing a test fixture and don't want to spin up a script.
Upgrading costs over time
One underused pattern: transparently rehash on login. When a user authenticates successfully, check whether the stored hash's cost is below your current target. If it is, hash the plaintext again at the new cost and update the record. Over time your whole user base drifts up to a stronger cost with zero forced password resets.
async function login(plain, storedHash) {
const ok = await bcrypt.compare(plain, storedHash);
if (!ok) return null;
const currentCost = bcrypt.getRounds(storedHash);
if (currentCost < SALT_ROUNDS) {
const upgraded = await bcrypt.hash(plain, SALT_ROUNDS);
await saveHash(upgraded);
}
return true;
}Bcrypt isn't glamorous, and it doesn't need to be. Store the hash, verify against it, tune the cost, and rehash on login. Get those four things right and you've handled password storage better than a depressing number of production systems out there.