Dev Utilities
How to Hash Passwords with Bcrypt: Salts, Cost Factors and Verification
A practical guide to bcrypt password hashing: why it beats SHA-256, how salts and cost factors work, and how to generate and verify a bcrypt hash.
Try the toolBcrypt Generator →Storing a password as plain text is an obvious mistake — but storing it as a fast hash like SHA-256 is a subtler one that is just as dangerous. If your database leaks, an attacker with a commodity GPU can test billions of SHA-256 guesses per second against your users' passwords. Bcrypt exists specifically to make that attack impractical. This guide explains how it works and how to use it correctly.
Why bcrypt instead of a plain hash
General-purpose hashes such as MD5 and SHA-256 are designed to be fast. That is exactly what you do not want for passwords. Bcrypt is deliberately slow and tunable, and it bakes in two defenses:
- A built-in salt. Every hash includes a random salt, so two users with the same password get different hashes. This defeats precomputed rainbow tables.
- A cost (work) factor. You choose how many rounds of key expansion run. As hardware gets faster, you raise the cost to keep brute-forcing expensive.
Anatomy of a bcrypt hash
A bcrypt hash is a single self-describing string. Everything a verifier needs — algorithm version, cost, salt and digest — lives inside it:
$2b$12$Nl1DXCq7iXZ2sK9m0oVv4uYh7q0m0m0m0m0m0m0m0m0m0m0m0m0mu
| | | |
| | | +-- 31-char hash
| | +-------------------------- 22-char salt
| +----------------------------- cost factor (2^12 rounds)
+-------------------------------- algorithm version (2b)
Because the salt and cost travel with the hash, you never store them separately — you just store this one string per user.
Generating and verifying in Node.js
The bcryptjs library is a pure-JavaScript implementation. A typical flow looks like this:
const bcrypt = require('bcryptjs');
// Hash on signup (cost factor 12)
const hash = bcrypt.hashSync('correct horse battery staple', 12);
// -> $2b$12$....
// Verify on login
const ok = bcrypt.compareSync('correct horse battery staple', hash);
console.log(ok); // true
const bad = bcrypt.compareSync('wrong password', hash);
console.log(bad); // false
Notice you never compare hashes with ===. Because each hash embeds its own random salt, the same password hashed twice produces two different strings. compareSync re-derives the hash using the salt stored inside the existing hash, then compares safely.
To experiment without wiring up a project, the Bcrypt Generator hashes a value at a cost factor you choose and lets you verify a plaintext against an existing hash — all in the browser, with nothing sent to a server.
Choosing a cost factor
The cost factor is exponential: each increment doubles the work. A rule of thumb is to pick the highest value that keeps hashing under roughly 250 ms on your production hardware:
- 10 — the historical default, still acceptable on modest hardware.
- 12 — a common modern baseline for web apps.
- 13–14 — stronger, if your servers can absorb the latency.
Common pitfalls
- Do not pre-hash with SHA-256 first. Feeding a hex digest into bcrypt can silently truncate entropy, and bcrypt ignores input beyond 72 bytes.
- Never reuse a manual salt. Let the library generate a fresh random salt for every hash — that is the whole point.
- Do not log the plaintext anywhere while hashing or verifying.
- Generate the passwords themselves securely. For seed accounts or test data, use a strong random source like the Password Generator rather than typing predictable strings.
Conclusion
Bcrypt turns a stolen database from an easy win into an expensive, time-boxed problem. Store the full hash string, pick a cost factor your servers can sustain, and always verify with the library's compare function rather than raw string equality. If you only need general-purpose digests for checksums or cache keys instead of passwords, reach for the Hash Generator instead.