If you're storing user passwords, you should not be storing passwords. You store a hash, and bcrypt is one of the safest, most boring choices you can make. Boring is good here. It's been around since 1999, it's battle-tested, and it does exactly the thing you need without much fuss.

What bcrypt actually is

Bcrypt is a password hashing function built on the Blowfish cipher. Unlike general-purpose hashes like SHA-256 or MD5, it's designed to be slow on purpose. That sounds like a bug, but it's the whole point. An attacker who steals your database has to spend real compute for every single guess, which makes brute-force attacks expensive.

Two features make bcrypt good for passwords:

  • A built-in salt. Every hash gets a random salt baked in, so two users with the same password get different hashes. No rainbow tables.
  • An adjustable cost factor. You choose how many rounds of work each hash takes. As hardware gets faster, you bump the cost up and stay ahead.

A bcrypt hash looks like this:

$2b$12$Nt7X... (60 characters total)

That string encodes everything you need to verify a password later: the algorithm version ($2b), the cost factor (12), the salt, and the hash itself. You don't store the salt separately — it travels with the hash.

When to use it (and when not to)

Use bcrypt for user account passwords. That's its home turf. If you're building a login system and reaching for a hashing library, bcrypt is a safe default that won't get you in trouble.

Where bcrypt is not the right tool:

  • Hashing large amounts of data or files. Bcrypt truncates input at 72 bytes and is intentionally slow. Use SHA-256 for integrity checks.
  • API keys and tokens. These are high-entropy random strings, not human-chosen passwords. A fast hash is fine, and often you don't need to hash them at all beyond a simple digest.
  • New greenfield projects with strict requirements. Argon2 is the modern winner of the Password Hashing Competition and is worth considering if your platform supports it well. That said, bcrypt is still perfectly acceptable and more widely available.

Common gotchas

The 72-byte limit

Bcrypt only looks at the first 72 bytes of input. Anything past that is silently ignored. For most passwords this never matters, but it becomes a real problem if you pre-hash or if you allow very long passphrases. A common pattern to sidestep it: run the password through SHA-256 first, then bcrypt the (base64-encoded) result. Just be consistent — do it the same way on signup and login.

Picking the cost factor

The cost factor is exponential. A cost of 12 does twice the work of 11. Too low and you're not protecting anything; too high and your login endpoint becomes a denial-of-service vector against yourself. Aim for a hash that takes somewhere around 100–250ms on your production hardware. Benchmark it — don't guess. A cost of 10–12 is a reasonable starting range in 2024, but measure on your actual servers.

Timing and comparison

Always use the library's built-in compare function to verify passwords. Never hash the input and compare strings yourself — the library handles constant-time comparison and correctly reads the cost and salt out of the stored hash.

Async vs sync

Bcrypt is CPU-heavy. In single-threaded runtimes like Node.js, the synchronous version blocks the event loop while it churns. Use the async API in request handlers so you don't stall every other user during a login.

A short worked example

Here's the typical flow in Node.js with the bcrypt package. Signup hashes the password; login verifies it.

const bcrypt = require('bcrypt');

const SALT_ROUNDS = 12;

// On signup
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');
// store `hash` in the users table

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

Notice you never store or manage the salt yourself — bcrypt.hash generates it and bcrypt.compare reads it back out of the stored string. That's the whole API surface you need for a login system.

The same pattern holds in every language. Python has bcrypt and passlib, PHP has password_hash() (which uses bcrypt by default), Go has golang.org/x/crypto/bcrypt. The function names change; the idea doesn't.

Trying it out quickly

Before wiring bcrypt into your app, it helps to see what the output actually looks like and how the cost factor changes things. You can generate hashes and inspect the format with our Bcrypt Hash Generator — handy for building test fixtures, debugging a mismatch, or just confirming a hash from your database was produced with the cost you expected.

Upgrading old hashes

One nice property: because the cost factor is stored in the hash itself, you can raise your security over time without forcing a password reset. On successful login, check the cost of the stored hash. If it's below your current target, re-hash the plaintext (which you have in hand at that moment) and update the record. Over weeks or months, active users get migrated to a stronger cost transparently.

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

  const currentCost = parseInt(storedHash.split('$')[2], 10);
  if (currentCost < SALT_ROUNDS) {
    const newHash = await bcrypt.hash(plain, SALT_ROUNDS);
    await db.updatePasswordHash(userId, newHash);
  }
  return true;
}

The short version

Bcrypt is a solid, well-understood choice for password storage. Let the library handle salting and comparison, pick a cost factor by benchmarking rather than copying a number off a forum, watch out for the 72-byte limit, and use the async API so you don't block your server. Do those four things and you've avoided nearly every mistake people make with password hashing.

Photo by FlyD on Unsplash