If you're storing user passwords and reaching for SHA-256, stop. Fast hashes are exactly what you don't want for passwords. You want something deliberately slow, salted, and tunable. Bcrypt fits that bill and has done so reliably for over two decades.

What bcrypt actually is

Bcrypt is a password hashing function based on the Blowfish cipher, designed by Niels Provos and David Mazières in 1999. Its whole point is to be computationally expensive. Each hash includes a cost factor (also called the work factor) that controls how many rounds of key expansion run. Bump the cost up and every hash takes longer to compute — for you and, crucially, for anyone trying to brute-force a stolen database.

A bcrypt hash is self-describing. Everything you need to verify a password later is baked into the string itself:

$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
 |  |  |                     |
 |  |  |                     +-- 31-char hash
 |  |  +------------------------ 22-char salt
 |  +--------------------------- cost factor (2^12 rounds)
 +------------------------------ algorithm version

Because the salt and cost live inside the output, you don't store them separately. You just keep the whole string in a column and hand it back to the library at verification time.

When and why you'd use it

Use bcrypt whenever you need to store user credentials and verify them later. Login systems, API key secrets, anything where you're checking "did the user type the right thing?" without ever needing to recover the original value.

The three properties that make it good for this:

  • Salting is automatic. Every hash gets a unique random salt, so two users with the same password produce completely different hashes. Rainbow tables are useless.
  • It's deliberately slow. You tune the cost so a single hash takes a noticeable fraction of a second on your hardware. That's painless for one login but brutal at scale for an attacker.
  • It's adjustable over time. As hardware gets faster, raise the cost. Old hashes still verify fine; you re-hash on next login.

Bcrypt is not the only option — argon2 and scrypt are both excellent and arguably stronger choices for new projects. But bcrypt is everywhere, battle-tested, and available in every language runtime you're likely to use. If you already have it, there's rarely a compelling reason to migrate.

A short worked example

Here's the typical flow in Node.js using the bcrypt package. Hashing on signup, comparing on login.

const bcrypt = require('bcrypt');

const COST = 12;

// On signup
async function hashPassword(plain) {
  return bcrypt.hash(plain, COST);
}

// On login
async function checkPassword(plain, stored) {
  return bcrypt.compare(plain, stored);
}

// Usage
const hash = await hashPassword('correct horse battery staple');
const ok = await checkPassword('correct horse battery staple', hash);
console.log(ok); // true

Note that you never compare hashes yourself with ===. The compare function re-derives the hash using the salt and cost embedded in the stored string, then does a constant-time comparison. Doing it by hand invites timing attacks and gets the salt handling wrong.

If you want to see what a bcrypt hash looks like for a given input and cost before wiring up code, the Bcrypt Hash Generator lets you produce and inspect hashes in the browser — handy for sanity-checking seed data or debugging a migration.

Common gotchas

The 72-byte limit

Bcrypt only looks at the first 72 bytes of the input. Anything beyond that is silently ignored. This matters if you let users submit very long passwords or if you pre-hash passphrases. A common pattern is to SHA-256 the password first and then bcrypt the result — but be careful: if you feed raw binary SHA output into bcrypt, null bytes can truncate it early in some implementations. Base64-encode the SHA digest first if you go this route.

Choosing the cost factor

There's no magic number. The right cost depends on your hardware and tolerance for latency. The usual advice is to target roughly 250–500ms per hash on your production servers. Benchmark it:

const start = Date.now();
await bcrypt.hash('test', 12);
console.log(`${Date.now() - start}ms`);

A cost of 10 was the default for years and is getting a bit light for modern hardware. Twelve is a reasonable baseline today. Measure rather than guess, and revisit it every year or two.

Don't pepper incorrectly

Some teams add a "pepper" — a secret value appended to passwords before hashing, stored outside the database (in config or an HSM). This can add defence in depth, but it introduces key-management complexity and the 72-byte limit still applies. If you do it, keep the pepper short and store it somewhere genuinely separate from the hashes.

Version and library mismatches

You'll see prefixes like $2a$, $2b$, and $2y$. These denote minor variations and bug fixes across implementations. Most modern libraries verify all of them correctly, but if you're moving hashes between languages (say, a PHP backend to a Node service), verify that the receiving library accepts the prefix your old system produced. Test with real data before you flip the switch.

Blocking the event loop

Bcrypt is CPU-bound by design. In single-threaded runtimes, use the async API so a login doesn't block everything else. The synchronous hashSync functions exist but should be reserved for scripts and tests, not request handlers.

Upgrading cost over time

Because the cost is stored in each hash, you can raise it gradually. On a successful login, check the embedded cost and re-hash if it's below your current target:

async function loginAndMaybeRehash(plain, stored, userId) {
  const ok = await bcrypt.compare(plain, stored);
  if (!ok) return false;

  if (bcrypt.getRounds(stored) < COST) {
    const fresh = await bcrypt.hash(plain, COST);
    await db.updatePasswordHash(userId, fresh);
  }
  return true;
}

This quietly upgrades your whole user base as people log in, without forcing a password reset.

Bcrypt isn't glamorous and it isn't new, but it remains a dependable default for credential storage. Pick a sensible cost, use the async library functions, respect the 72-byte limit, and lean on the library for comparison. Get those right and you've avoided the vast majority of password-storage mistakes.

Photo by FlyD on Unsplash