Storing passwords as plain text is the kind of mistake that makes the news. You already know not to do that. The next trap is reaching for a general-purpose hash like SHA-256 and calling it a day. That's almost as bad, because fast hashes are exactly what an attacker wants when they're grinding through a stolen database.
Bcrypt solves a specific problem: making password verification deliberately slow and memory-aware enough that brute force stops being cheap. It has been around since 1999, it's battle-tested, and it's available in pretty much every language runtime. For most applications it's still the right default.
What bcrypt actually does
Bcrypt is a password-hashing function built on the Blowfish cipher. Three things make it useful for passwords:
- It's slow on purpose. A tunable cost factor (also called work factor or rounds) controls how many iterations the key-setup runs. Bump the cost and every hash gets more expensive to compute.
- It salts automatically. Every hash includes a random salt, so identical passwords produce different hashes. You don't manage salts yourself.
- It's self-describing. The output string contains the algorithm version, cost, salt, and digest all in one field.
A bcrypt hash looks like this:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUWBreak it down: $2b$ is the algorithm version, 12 is the cost factor, the next 22 characters are the salt, and the rest is the actual digest. Because everything you need to verify a password lives in that one string, you store a single column and nothing else.
When to use it (and when not to)
Use bcrypt whenever you're storing user passwords in a database. It's the sensible, boring choice that won't get you in trouble.
It is not for hashing large files, API keys, or anything where speed matters. Bcrypt being slow is a feature for passwords and a bug for everything else. It also caps input at 72 bytes, so it's unsuitable for hashing long data.
If you're starting a greenfield project and want the current state of the art, Argon2id is worth a look — it adds memory-hardness that resists GPU attacks better. But bcrypt remains perfectly acceptable, and its ubiquity means you'll rarely hit a platform that doesn't support it. If your stack already uses bcrypt, there's no urgent reason to migrate.
Common gotchas
The 72-byte truncation
Bcrypt silently ignores everything past 72 bytes of input. If users have very long passwords, the tail gets dropped. The usual fix is to pre-hash with SHA-256 and base64-encode the result before feeding it to bcrypt, so even huge passwords collapse to a fixed-length input. Just be consistent — do it on both signup and login.
Picking a cost factor
The cost is a logarithmic value: cost 12 does twice the work of cost 11. A common target is roughly 250ms per hash on your production hardware. On modern servers that's often cost 12 or 13. Don't blindly copy a number from a tutorial — benchmark on your own machines, because a cost that's fine on a beefy server might time out requests on a tiny container.
Null bytes
Some older bcrypt implementations stop reading at a null byte. If a password contains one, everything after it is ignored. Modern libraries handle this correctly, but it's a reason to keep your library up to date.
Comparing hashes
Never compare password hashes with a plain ==. Use the library's verify function, which handles the salt extraction and does a constant-time comparison to avoid timing leaks.
A worked example
Here's the full round trip in Node.js using the bcrypt package. The pattern is the same in every language: hash on signup, verify on login.
const bcrypt = require('bcrypt');
const COST = 12;
// On signup
async function hashPassword(plain) {
return bcrypt.hash(plain, COST);
}
// On login
async function verifyPassword(plain, storedHash) {
return bcrypt.compare(plain, storedHash);
}
(async () => {
const hash = await hashPassword('correct horse battery staple');
console.log(hash);
// $2b$12$... store this single string in your users table
console.log(await verifyPassword('correct horse battery staple', hash)); // true
console.log(await verifyPassword('wrong password', hash)); // false
})();Notice there's no separate salt column, no manual concatenation, no comparison logic you have to get right. You store the string from hash() and later hand it to compare(). That's the whole contract.
In Python the equivalent with bcrypt looks like this:
import bcrypt
pw = b'correct horse battery staple'
hashed = bcrypt.hashpw(pw, bcrypt.gensalt(rounds=12))
bcrypt.checkpw(pw, hashed) # TrueIf you just want to eyeball a hash, experiment with cost factors, or generate a test value without spinning up a project, our Bcrypt Hash Generator produces valid hashes in the browser. Handy for quick debugging or seeding a dev database.
Upgrading cost over time
Hardware gets faster, so a cost that was expensive five years ago is cheap now. A good practice is to re-hash passwords transparently at login. Since the cost is embedded in the stored hash, you can check it after a successful verify and, if it's below your current target, re-hash the plaintext you already have in hand:
async function loginAndMaybeUpgrade(plain, storedHash, save) {
const ok = await bcrypt.compare(plain, storedHash);
if (!ok) return false;
const currentCost = parseInt(storedHash.split('$')[2], 10);
if (currentCost < COST) {
await save(await bcrypt.hash(plain, COST));
}
return true;
}This quietly keeps your stored hashes current without forcing a mass password reset.
The short version
Bcrypt gives you salting, a tunable work factor, and a compact self-describing output for free. Use it for passwords, never for general hashing. Watch the 72-byte limit, benchmark your cost factor on real hardware, always verify with the library's compare function, and re-hash on login to stay ahead of faster attackers. Get those few things right and your password storage will hold up far better than most of the breaches you read about.
Photo by Jake Walker on Unsplash