If you're storing passwords, you need a slow, salted hash. Bcrypt has been the sensible default for that for over two decades, and it still holds up. It's not the newest algorithm on the block, but it's battle-tested, available in every language, and hard to misuse in a catastrophic way.
Here's what you actually need to know to use it correctly.
What bcrypt actually is
Bcrypt is a password hashing function built on the Blowfish cipher. Two things make it good for passwords:
- It's deliberately slow. A fast hash like SHA-256 is a liability for passwords because an attacker with a leaked database can try billions of guesses per second. Bcrypt has a tunable cost factor that makes each hash take a measurable amount of time.
- It salts automatically. Every hash includes a random salt, so two identical passwords produce different hashes. You don't manage the salt separately — it's baked into the output string.
A bcrypt hash looks like this:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUWBreaking that down: $2b$ is the algorithm version, 12 is the cost factor, the next 22 characters are the salt, and the rest is the actual hash. Everything you need to verify a password later lives in that one string.
When to use it
Use bcrypt whenever you store user passwords and need to verify them later. That's the core use case, and it's a big one.
It's also fine for anything where you're checking a secret against a stored value — API keys you control, one-time tokens you want to protect at rest. What bcrypt is not for:
- General-purpose hashing (checksums, deduplication, cache keys) — use SHA-256 or similar, which is meant to be fast.
- Hashing large amounts of data. Bcrypt is for short secrets.
- Encryption. Hashing is one-way. If you need to get the original value back, you want encryption, not a hash.
If you're starting fresh and your platform supports it well, Argon2 is the modern recommendation and worth a look. But bcrypt remains a completely defensible choice, and it's what a huge amount of production software runs on today.
The cost factor
The cost (or "work factor") is the one knob you tune. Each increment doubles the work. A cost of 12 does twice the computation of 11. The goal is to make it slow enough to frustrate attackers but fast enough that your login endpoint doesn't crawl.
A cost of 10 to 12 is a reasonable starting point on modern hardware. The right number depends on your servers — aim for something in the 100–250ms range per hash. Benchmark it. And revisit it every couple of years, because hardware keeps getting faster.
Common gotchas
The 72-byte limit
Bcrypt only looks at the first 72 bytes of the input. Anything beyond that is silently ignored. For normal passwords this is a non-issue, but it surprises people. If you want to support arbitrarily long passphrases, pre-hash with SHA-256 first and feed the result to bcrypt — just be consistent about it.
Null byte truncation
Some older bcrypt implementations truncate at the first null byte. If you pre-hash and pass raw binary, you can hit this. Base64-encode the pre-hash output to be safe.
Comparing hashes yourself
Never do if (hash === storedHash) in your login logic. Always use the library's built-in compare function. It re-derives the hash using the salt and cost embedded in the stored value, and it uses a constant-time comparison to avoid timing attacks.
Storing the cost separately
You don't need to. The cost and salt are in the hash string. Store the whole string, verify against the whole string, and you're done. Migrating to a higher cost later is easy: on successful login, re-hash the plaintext at the new cost and update the stored value.
A worked example
Here's the full round trip in Node.js using the bcrypt package:
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// When a user signs up or changes their password
async function hashPassword(plain) {
return bcrypt.hash(plain, SALT_ROUNDS);
}
// When a user logs in
async function verifyPassword(plain, storedHash) {
return bcrypt.compare(plain, storedHash);
}
// Usage
const hash = await hashPassword('correct horse battery staple');
console.log(hash);
// $2b$12$... store this whole string in your DB
const ok = await verifyPassword('correct horse battery staple', hash);
console.log(ok); // trueNotice there's no separate salt handling anywhere. bcrypt.hash generates a random salt, and bcrypt.compare pulls it back out of the stored string. That simplicity is a big part of why bcrypt is hard to mess up.
The same pattern exists everywhere. In Python it's bcrypt.hashpw and bcrypt.checkpw. In PHP, password_hash($pw, PASSWORD_BCRYPT) and password_verify. Go has golang.org/x/crypto/bcrypt. The function names change; the concepts don't.
Trying it out quickly
When you want to eyeball a hash, test your understanding of the format, or generate a value for a quick experiment without wiring up code, a browser tool is handy. Our Bcrypt Hash Generator lets you produce a bcrypt hash at a chosen cost factor and see the output structure right away — useful for debugging seed data or checking that your app's format matches what you expect.
Quick checklist
- Use bcrypt (or Argon2) for passwords, never a plain fast hash.
- Pick a cost factor that takes ~100–250ms on your hardware; benchmark it.
- Store the entire hash string — salt and cost are included.
- Always verify with the library's compare function, never manual string equality.
- Watch the 72-byte input limit if you support long passphrases.
- Re-hash at a higher cost on login as hardware improves.
Password hashing is one of the few areas where the boring, well-understood option is genuinely the right one. Reach for bcrypt, tune the cost, let the library handle the salt, and move on to the harder problems.