If you're storing user passwords and you're not sure what to hash them with, bcrypt is a solid, boring, correct answer. It's been around since 1999, it's well understood, and it's available in every language you're likely to be writing in. You don't need to be a cryptographer to use it well — but there are a few sharp edges worth knowing about before you ship.

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 slow. That sounds like a bug until you remember the threat model: someone steals your database and tries to brute-force every hash offline. A fast hash lets an attacker try billions of guesses per second on a GPU. Bcrypt makes each guess expensive, so brute force stops being practical.

Two features make it good for passwords:

  • Adjustable work factor. A "cost" parameter controls how many rounds of hashing happen. Higher cost = slower = harder to crack. As hardware gets faster, you bump the cost.
  • Built-in salting. Bcrypt generates a random salt for each password and embeds it in the output. No two identical passwords produce the same hash, which kills rainbow-table attacks.

Reading a bcrypt hash

A bcrypt hash is a single self-contained string that looks like this:

$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW

Broken down:

  • $2b$ — the algorithm version.
  • 12 — the cost factor (2^12 = 4096 iterations).
  • The next 22 characters — the salt.
  • The rest — the actual hash.

Because the salt and cost are baked into the string, you don't need separate columns for them. Store one VARCHAR(60) and you're done.

When to use it (and when not to)

Use bcrypt whenever you need to store a secret that a human will later re-enter — passwords, PINs, some API secrets. It's designed for exactly this.

Don't use it for things where speed matters or where the input isn't a low-entropy human secret: don't bcrypt a file to verify integrity (use SHA-256 for that), and don't bcrypt session tokens or random API keys — those are already high-entropy, so a fast hash is fine and appropriate.

If you're starting a greenfield project and want the current "best" answer, Argon2id is the modern winner of the Password Hashing Competition and is memory-hard on top of being slow. But bcrypt is still perfectly acceptable, more widely supported, and harder to misconfigure. If your framework ships bcrypt as the default, there's no urgent reason to swap.

A short worked example

Here's the typical flow in Node.js using the bcrypt package. The pattern is identical in every language: hash on signup, compare on login.

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');
const ok = await verifyPassword('correct horse battery staple', hash);
console.log(ok); // true

Notice you never decrypt anything. Bcrypt is one-way. To check a password you re-hash the input using the salt and cost stored in the existing hash, then compare. The compare function handles extracting those for you.

Want to eyeball what a hash looks like before wiring it into code, or generate a quick one for a test fixture? Our Bcrypt Hash Generator lets you produce and verify hashes in the browser, which is handy when you're debugging a login that keeps returning false.

Common gotchas

The 72-byte limit

This one surprises everyone. Bcrypt silently ignores everything after the first 72 bytes of input. So these two passwords hash to the same value if the first 72 bytes match:

aaaa...(72 chars)...aaaa
aaaa...(72 chars)...aaaaEXTRA_STUFF

For normal passwords this never matters. But if you "pre-hash" long inputs, or accept very long passphrases, be aware. The common fix is to run the input through SHA-256 first and pass the (base64-encoded) digest to bcrypt — just be consistent about it across signup and login.

Choosing the wrong cost

A cost of 10 was fine years ago; 12 is a reasonable default today. Aim for roughly 200–300ms per hash on your production hardware. Too low and you're not protecting anyone; too high and your login endpoint becomes a denial-of-service vector against yourself. Benchmark it on the machine that'll actually run it, not your laptop.

Using == instead of a constant-time compare

Always use the library's compare function, never a plain string equality on hashes. Library comparisons are timing-safe, which prevents an attacker from learning information byte-by-byte through response timing.

Hashing on the client

Don't. If the client sends a bcrypt hash and you store it directly, that hash becomes the password — anyone with the database can log in by replaying it. Hash on the server, over HTTPS.

Forgetting to rehash on cost upgrades

When you raise the cost factor, existing hashes still use the old one. The clean way to migrate: on successful login, check the cost embedded in the stored hash, and if it's below your current target, re-hash the plaintext (which you have in hand at that moment) and update the row. Users get upgraded transparently as they log in.

The short version

Bcrypt does one job and does it well: makes stolen passwords expensive to crack. Pick a cost around 12, let the library handle salting and comparison, watch the 72-byte limit, and never hash on the client. Get those right and you've covered the failure modes that account for the vast majority of real-world password-storage mistakes.

If Argon2id is available in your stack and you're building something new, reach for it. If not, bcrypt remains a completely defensible choice that's very hard to get catastrophically wrong.

Photo by FlyD on Unsplash