mrkeyoor.com_
Wed 05 Aug 05:01 UTC
npmSecurityupdated 05 Aug 2026

bcrypt

bcrypt is the classic Node.js library for hashing passwords. It is a native addon wrapping the OpenBSD bcrypt implementation: you pick a cost factor (rounds), call hash() to get a salted 60-character hash, and later call compare() to check a login attempt. The cost factor makes hashing deliberately slow (2^rounds iterations), which is the whole point: it makes brute-forcing stolen hashes expensive. The async API runs hashing on a thread pool so it does not block the event loop. It does one job, has done it for over a decade, and the API is essentially frozen.

Verdict

Battle-tested and fine for existing systems with bcrypt hashes, but it is no longer the default answer for new code: argon2 is the stronger algorithm and bcryptjs removes the native-build risk. Pick bcrypt for compatibility, not by reflex.

API stability5/5hash, compare, genSalt, and getRounds have not meaningfully changed in years; v6 was mostly a platform floor bump to Node 18, and old hashes stay verifiable.
Docs4/5The README is genuinely good: full API reference, cost-factor timing table, security history, and the 72-byte warning; there is just no docs site beyond it.
Maintenance3/5Alive but slow: last release v6.0.0 in May 2025, last repo push April 2026, 38 open issues and PRs; fine for a frozen-scope library, bad if you expect quick fixes.
Ecosystem4/5About 5.7M weekly downloads and every Node auth tutorial uses it, but the mindshare is shifting toward argon2 and pure-JS or Rust-based alternatives.

Use it if

  • You store user passwords and need the industry-default hash with salt handling built in
  • You inherit a database of existing bcrypt hashes ($2a$ or $2b$ prefixes) and need compatibility
  • You run on standard Node 18+ server platforms where the prebuilt native binaries just work
  • You want hashing off the event loop: the async API uses a worker thread pool
Skip it if

Setup reality

On Windows x64/arm64, mainstream Linux (glibc and musl) x64/arm64, and macOS, npm install bcrypt pulls a prebuilt binary via node-gyp-build and you are done. Off that happy path (unusual arch, brand-new Node release before prebuilds land, restricted networks), it compiles from source, which drags in node-gyp, Python, and a C++ toolchain, and on Windows the Visual Studio C++ workload. v6 requires Node 18+. Also plan for the 72-byte input truncation and the fact that TypeScript types are a separate @types/bcrypt install.

Patterns

Hash a password before storing ithash-password

import bcrypt from 'bcrypt';

const saltRounds = 10;
const hash = await bcrypt.hash(plainPassword, saltRounds);
// store `hash` in your DB; the salt is embedded in it

Never store the plain password anywhere; the 60-char hash string contains algorithm, cost, and salt, so no separate salt column is needed.

Check a login attempt against a stored hashverify-password

const ok = await bcrypt.compare(plainPassword, user.passwordHash);
if (!ok) {
  return res.status(401).json({ error: 'Invalid credentials' });
}

compare() extracts cost and salt from the stored hash automatically; never compare hash strings with === yourself.

Pick a cost factor deliberatelychoose-cost-factor

// rounds means 2^rounds iterations. README ballpark on a 2GHz core:
// 10 -> ~10 hashes/sec, 12 -> 2-3 hashes/sec, 13 -> ~1 sec/hash
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);

Benchmark on your production hardware and pick the highest rounds your login latency budget tolerates; 10 is the default, many teams now run 12.

Use async, not the Sync variants, on serversavoid-sync-blocking

// BAD on a server: blocks the event loop for the full hash time
const hash = bcrypt.hashSync(password, 12);

// GOOD: runs on the worker thread pool
const hash2 = await bcrypt.hash(password, 12);

At cost 12 hashSync freezes your process for hundreds of milliseconds per call; the README itself recommends the async API on servers.

Guard against the 72-byte truncationhandle-72-byte-limit

const BYTE_LIMIT = 72;
if (Buffer.byteLength(password, 'utf8') > BYTE_LIMIT) {
  // bytes beyond 72 are silently ignored by bcrypt
  return res.status(400).json({ error: 'Password too long (72 bytes max)' });
}
const hash = await bcrypt.hash(password, 12);

The limit is bytes, not characters: emoji and non-Latin scripts hit it far sooner than 72 characters.

Transparently upgrade old hashes at loginupgrade-rounds-on-login

const TARGET_ROUNDS = 12;
const ok = await bcrypt.compare(password, user.passwordHash);
if (ok && bcrypt.getRounds(user.passwordHash) < TARGET_ROUNDS) {
  const newHash = await bcrypt.hash(password, TARGET_ROUNDS);
  await users.updateOne({ _id: user._id }, { $set: { passwordHash: newHash } });
}

Login is the only moment you hold the plain password, so it is the only place you can re-hash at a higher cost.

Registration route with hashingexpress-register-route

app.post('/register', async (req, res) => {
  const { email, password } = req.body;
  const passwordHash = await bcrypt.hash(password, 12);
  await users.insertOne({ email, passwordHash });
  res.status(201).json({ ok: true });
});

Do the hashing inside the request handler with await; wrapping hashSync in a route is the classic accidental event-loop stall.

Generate a salt as a separate stepgenerate-salt-manually

const salt = await bcrypt.genSalt(12);
const hash = await bcrypt.hash(password, salt);

Functionally identical to passing rounds straight to hash(); only useful when you want to log or reuse the salt string.

Inspect the cost factor of a stored hashread-hash-rounds

const rounds = bcrypt.getRounds('$2b$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa');
console.log(rounds); // 10

Handy for auditing a user table to see how many hashes still sit at an outdated cost factor.

Avoid user-enumeration via response timingdummy-compare-timing

const DUMMY_HASH = await bcrypt.hash('dummy-password', 12); // do once at boot

const user = await users.findOne({ email });
const hashToCheck = user ? user.passwordHash : DUMMY_HASH;
const ok = await bcrypt.compare(password, hashToCheck);
if (!user || !ok) return res.status(401).json({ error: 'Invalid credentials' });

Without a dummy compare, unknown emails return instantly while known ones take ~100ms, which leaks which emails have accounts.

Alternatives

PackageRegistryPick it when
argon2npmNew projects: Argon2id is the current OWASP first choice and this binding is well maintained
bcryptjsnpmYou need bcrypt-compatible hashes without any native compilation (edge, Electron, odd platforms)
@node-rs/bcryptnpmYou want bcrypt semantics with Rust-built prebuilds and no node-gyp in sight