If you store passwords, you hash them. If you hash passwords, bcrypt is still one of the safest defaults you can reach for. It has been around since 1999 and it keeps holding up, mostly because it was designed to be slow on purpose. That sounds like a bug. It's the whole point.
What bcrypt actually is
bcrypt is a password-hashing function built on the Blowfish cipher. Unlike a general-purpose hash like SHA-256, it's deliberately expensive to compute. That deliberate cost is controlled by a work factor (also called the cost or rounds). Every time you increase the work factor by one, the computation takes roughly twice as long.
Why do you want slow? Because attackers who steal your database will try billions of guesses. A fast hash lets them test the entire English dictionary plus common variations in seconds. A slow hash forces them to spend real time and real hardware on each attempt. That's the trade: your login endpoint takes 100ms instead of 1ms, and an offline cracking attack becomes economically painful.
bcrypt also handles salting for you. A salt is random data mixed into each password before hashing, so two users with the same password get different hashes. bcrypt generates a salt automatically and stores it inside the output string, which is one of the reasons it's so pleasant to use.
Reading a bcrypt hash
A bcrypt hash looks like this:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUWIt's not gibberish — it's four fields packed together:
$2b$— the algorithm version12$— the work factor (cost)- the next 22 characters — the salt
- the rest — the actual hash
Because everything you need to verify a password lives in that single string, you store exactly one column in your database. No separate salt field, no bookkeeping.
When to use it (and when not to)
Use bcrypt for passwords and other low-entropy secrets that humans choose. That's its job.
Do not use bcrypt — or any password hash — for things like API keys, session tokens, or file integrity checks. Those need speed and different properties. For hashing arbitrary data, use SHA-256. For high-entropy secrets you generated yourself, a fast hash is fine because there's nothing to brute-force.
bcrypt's main modern competitors are Argon2 and scrypt. Argon2 (specifically Argon2id) won the Password Hashing Competition and is a great choice for new projects because it resists GPU and memory-hard attacks better. That said, bcrypt is battle-tested, available in every language, and perfectly acceptable. If you already use it, there's rarely a reason to migrate in a panic.
A worked example
Here's the full cycle 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$... store this string in your users table
const ok = await verifyPassword('correct horse battery staple', hash);
console.log(ok); // trueNotice you never handle the salt yourself. bcrypt.hash creates one, bcrypt.compare reads it back out of the stored string. That's the design working for you.
The Python equivalent with passlib is just as short:
from passlib.hash import bcrypt
hashed = bcrypt.using(rounds=12).hash("correct horse battery staple")
bcrypt.verify("correct horse battery staple", hashed) # TrueIf you just want to eyeball what a bcrypt hash looks like or test a value quickly without wiring up a project, our Bcrypt Hash Generator lets you produce and inspect hashes right in the browser.
Common gotchas
The 72-byte limit
bcrypt only looks at the first 72 bytes of the input. Anything beyond that is silently ignored. Most passwords are nowhere near that long, but it becomes a real security bug if you pre-hash passwords or accept passphrases. A common footgun: some people run sha256 on the password first, then bcrypt the hex output — but hex is longer per byte, so you can waste your budget. If you must pre-hash to bypass the limit, base64-encode a raw digest rather than hex.
Choosing the work factor
Don't hardcode a number and forget it. A cost of 10 was fine years ago; 12 is a reasonable modern baseline. The right value depends on your hardware — aim for a hash to take somewhere between 100ms and 300ms on your production servers. Benchmark it. As hardware gets faster over the years, bump the cost.
Rehashing on login
Because the cost is stored in the hash, you can upgrade users transparently. When someone logs in successfully and their stored hash uses an old cost, rehash the plaintext (which you have at that moment) with the new cost and save it:
if (await bcrypt.compare(plain, storedHash)) {
if (bcrypt.getRounds(storedHash) < SALT_ROUNDS) {
const upgraded = await bcrypt.hash(plain, SALT_ROUNDS);
await saveHash(user.id, upgraded);
}
// proceed with login
}Null bytes
Some older bcrypt implementations truncate at the first null byte in the input. It's rare to hit this with normal passwords, but if you ever process binary-ish input, be aware of it.
Timing and comparison
Always compare with the library's own function (bcrypt.compare), never by re-hashing and doing a string ===. The library handles constant-time comparison and re-derives the salt correctly. Rolling your own comparison invites both bugs and timing leaks.
The short version
bcrypt is slow, salted, and self-contained — three qualities that make it a solid choice for storing passwords. Pick a work factor that takes a couple hundred milliseconds, watch out for the 72-byte limit, rehash on login as your cost grows, and lean on the library for hashing and comparison. Do those four things and you've handled the part of authentication that most often goes wrong.