If you're storing user passwords, you don't store passwords. You store hashes. And if you're reaching for bcrypt to do it, you've picked a solid, boring, well-tested tool — which is exactly what you want for security.

The trouble is that a lot of tutorials treat bcrypt like a black box: call one function, done. That works right up until you hit a truncation bug, a login endpoint that takes 800ms, or a cost factor that made sense in 2013. Here's what's worth knowing before you ship it.

What bcrypt actually is

Bcrypt is a password hashing function built on the Blowfish cipher, published back in 1999. Unlike general-purpose hashes like SHA-256, it was designed specifically for passwords, which means it's deliberately slow and includes a built-in salt.

Those two properties matter:

  • It's adaptively slow. Bcrypt has a cost factor (also called work factor or rounds). Each increment doubles the work required to compute a hash. As hardware gets faster, you bump the cost to keep attackers honest.
  • The salt is baked in. Bcrypt generates a random salt and stores it inside the output string. You don't manage salts separately, and you don't need a separate column for them.

A bcrypt hash looks like this:

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

That single string encodes everything: $2b$ is the algorithm version, 12 is the cost factor, the next 22 characters are the salt, and the rest is the actual hash. When you verify a password, bcrypt reads the cost and salt straight out of the stored string. No extra bookkeeping.

When to use it (and when not to)

Use bcrypt when you're storing credentials that humans type — login passwords, mostly. It's the sensible default for the vast majority of web apps. It's widely supported, battle-tested, and hard to misuse in a dangerous way.

Don't use bcrypt for:

  • Hashing large files or arbitrary data. That's what SHA-256 is for. Bcrypt is slow on purpose and caps input length.
  • API keys or high-entropy random tokens. If a secret is 256 bits of randomness, a fast hash (or even a constant-time comparison of the raw value) is fine. The slowness of bcrypt buys you nothing there.

Worth mentioning: newer algorithms like Argon2 and scrypt are also excellent and resist GPU attacks better. If you're starting fresh and your platform supports Argon2id cleanly, it's a great choice. But bcrypt is not wrong, and it's often the path of least resistance in an existing stack. Don't rip out working bcrypt code just to chase fashion.

The gotchas that actually bite people

1. The 72-byte truncation

Bcrypt silently ignores anything past the first 72 bytes of input. If your users have very long passphrases, or you're pre-hashing with a passphrase manager, two different passwords that share the first 72 bytes will hash to the same thing. The common fix is to pre-hash the password with SHA-256 and base64-encode the result before passing it to bcrypt, giving you a fixed-length input well under the limit.

2. Null bytes

Some older bcrypt implementations truncate at the first null byte. Combined with the pre-hashing trick above, this can cause surprises. Base64-encoding your SHA-256 digest avoids raw null bytes entirely.

3. Picking a cost factor blind

A cost of 10 might have been reasonable years ago. Higher isn't automatically better — a cost of 15 could make each login take over a second and turn your auth endpoint into a denial-of-service target against yourself. Benchmark on your production hardware and aim for roughly 200–350ms per hash. That's slow enough to hurt attackers, fast enough that users don't notice.

4. Not using constant-time comparison

Always use the library's built-in compare/verify function rather than hashing the input and comparing strings yourself. The library handles timing-safe comparison and reads the cost and salt from the stored hash for you.

A short worked example

Here's the typical flow in Node.js with the bcrypt package. Same shape applies in Python, PHP, Go, and everywhere else.

const bcrypt = require('bcrypt');

const SALT_ROUNDS = 12;

// Registration: hash and store
async function hashPassword(plain) {
  return bcrypt.hash(plain, SALT_ROUNDS);
}

// Login: verify against the stored hash
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 what you don't do: you never generate or store a salt yourself, and you never compare hashes by hand. The stored hash carries the cost and salt inside it, so bcrypt.compare can re-derive everything it needs.

When you want to eyeball what a hash looks like, tweak the cost factor, or verify a value without wiring up code, our Bcrypt Hash Generator lets you produce and inspect hashes right in the browser — handy for debugging a mismatch or sanity-checking a migration.

Upgrading cost over time

Because the cost is stored in the hash, you can migrate users transparently. On a successful login, check whether the stored hash uses your current cost:

async function verifyAndUpgrade(plain, storedHash) {
  const ok = await bcrypt.compare(plain, storedHash);
  if (!ok) return { ok: false };

  // Re-hash if the stored cost is behind current policy
  const rounds = bcrypt.getRounds(storedHash);
  if (rounds < SALT_ROUNDS) {
    const newHash = await bcrypt.hash(plain, SALT_ROUNDS);
    return { ok: true, newHash };
  }
  return { ok: true };
}

Since you only have the plaintext at login time, this is the natural moment to re-hash. Store the new hash and you've quietly upgraded that user's security with zero disruption.

The short version

  • Use bcrypt (or Argon2id) for passwords, never a bare fast hash.
  • Let the library handle salting and comparison — don't roll your own.
  • Watch the 72-byte limit; pre-hash long inputs if needed.
  • Tune the cost to your hardware, aiming for a few hundred milliseconds.
  • Re-hash on login to upgrade cost over time.

Get those five things right and your password storage is in good shape. The tooling has been solid for two decades — the mistakes are almost always in how it's wired up, not in bcrypt itself.

Photo by FlyD on Unsplash