Storing passwords is one of those things you either get right once and forget about, or get wrong and end up in a breach post-mortem. Bcrypt is the boring, correct choice for most applications, and it has been for over two decades. Here's how it works and how to use it without shooting yourself in the foot.

What bcrypt actually is

Bcrypt is a password-hashing function built on the Blowfish cipher. It was designed in 1999 specifically to be slow and resistant to brute-force attacks. That's the key idea: a general-purpose hash like SHA-256 is fast, which is great for checksums and terrible for passwords. If an attacker steals your database, a fast hash lets them try billions of guesses per second on a GPU.

Bcrypt fights back in two ways:

  • A built-in salt. Every hash includes a random salt, so two identical passwords produce different hashes. This kills precomputed rainbow-table attacks.
  • An adjustable cost factor. You choose how many rounds of work bcrypt performs. Higher cost means slower hashing, which means slower cracking. As hardware gets faster, you bump the cost.

Reading a bcrypt hash

A bcrypt hash is self-describing. Here's a real example:

$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│  │  └── salt (22 chars) + hash (31 chars)
│  └───── cost factor (12)
└──────── algorithm version

The $2b$ prefix is the version. 12 is the cost. Everything after is a Base64-encoded salt followed by the hash. Because the salt and cost live inside the string, you don't store them separately — you just save the whole thing in a single column.

When to reach for it

Use bcrypt whenever you're storing user passwords and don't have a strong reason to use something else. It's well understood, available in every major language, and hard to misuse in a dangerous way.

The modern alternatives worth knowing about are argon2 (the current password-hashing competition winner, and a great default if your platform supports it well) and scrypt. Argon2 is memory-hard, which makes GPU and ASIC attacks harder still. If you're starting fresh and argon2 is readily available in your stack, it's a fine choice. But bcrypt remains completely acceptable and is often the path of least resistance. Don't rip out working bcrypt to chase a marginal upgrade.

What you should never use for passwords: plain MD5, SHA-1, SHA-256, or SHA-512 on their own. They're fast by design. Speed is exactly the wrong property here.

A short worked example

Here's the typical flow in Node.js using the bcrypt package. Registration hashes the password; login compares.

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 verify(plain, storedHash) {
  return bcrypt.compare(plain, storedHash);
}

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

Notice you never write your own comparison logic. bcrypt.compare extracts the salt and cost from the stored hash, re-hashes the input, and does a constant-time comparison for you. Don't do hash(input) === storedHash yourself — you'll get the salt handling wrong and open a timing hole.

If you just want to see what a hash looks like or generate one for testing without writing any code, our Bcrypt Hash Generator lets you produce a hash from a string and pick the cost factor in the browser.

Gotchas that bite people

The 72-byte limit

Bcrypt only looks at the first 72 bytes of input. Anything past that is silently ignored. This matters if you let users have very long passwords, or worse, if you pre-hash passwords with something and feed the result in. A common footgun: pre-hashing with SHA-256 and Base64-encoding produces a long string, but the entropy might collapse in unexpected ways. If you need to support long inputs, pre-hash with SHA-256 and encode as Base64 only if you understand the trade-offs — most apps don't need this at all.

Null bytes

Some older bcrypt implementations truncate at the first null byte. If a password contains a \0, everything after gets dropped. Modern libraries handle this, but it's worth knowing if you're on legacy code.

Choosing a cost factor

Higher is safer but slower. The right value depends on your hardware and how much login latency you can tolerate. A good rule of thumb: pick the highest cost that keeps a single hash under about 250–350ms on your production servers. On modern hardware, 12 is a sensible default; 13 or 14 if you have headroom. Benchmark it — don't guess.

// Rough benchmark
const start = Date.now();
await bcrypt.hash('test', 12);
console.log(Date.now() - start, 'ms');

Rehashing on login

Because the cost is baked into the hash, you can upgrade users gradually. When someone logs in successfully, check whether the stored hash's cost is below your current target. If it is, rehash their password with the new cost and save it. Over time your whole user base migrates without a forced reset.

Don't log the plaintext

Obvious, but it happens. Make sure your request logging, error tracking, and analytics never capture the raw password. Redact password fields at the framework level so a stray console.log(req.body) doesn't leak everything.

The short version

  • Bcrypt is slow on purpose — that's the feature.
  • Store the whole hash string; the salt and cost are inside it.
  • Use the library's compare, never roll your own equality check.
  • Cost 12 is a reasonable default; benchmark and rehash on login to keep up.
  • Watch the 72-byte limit and never store or log plaintext.

Get those five things right and password storage stops being something you have to worry about.

Photo by FlyD on Unsplash