bcrypt Password Hashing — Why You Should Use bcrypt and How to Implement It
bcrypt is the standard password hashing algorithm for web applications. Learn why MD5 and SHA-256 are wrong for passwords, how bcrypt's work factor prevents brute-force...
bcrypt is the default choice for password hashing because it’s slow by design — slowing attackers attempting to crack stolen hashes. Using MD5 or SHA-256 for passwords is a critical security mistake.
Generate random salts and test hashes with the Hash Generator.
Why not SHA-256 for passwords?
// ❌ Wrong: SHA-256 is fast — attackers can try billions/second
const hash = crypto.createHash('sha256').update('password').digest('hex');
// An RTX 4090 can crack simple passwords in minutes
// ❌ Wrong: Adding salt doesn't solve the speed problem
const hash = sha256(salt + password);
// Still crackable via GPU acceleration at billions of iterations/second
// ✅ Correct: bcrypt is intentionally slow (~100ms per hash)
const hash = await bcrypt.hash(password, 12);
// At cost factor 12: ~250ms/hash → attacker rate: ~4 hashes/second
bcrypt in Node.js
npm install bcrypt
# or the pure-JS alternative:
npm install bcryptjs
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // Work factor (log2 of iterations)
// Hash a password:
async function hashPassword(plaintext) {
const hash = await bcrypt.hash(plaintext, SALT_ROUNDS);
// "$2b$12$..." — includes algorithm, cost factor, salt, and hash
return hash;
}
// Verify a password:
async function verifyPassword(plaintext, hash) {
return bcrypt.compare(plaintext, hash);
// Returns true/false — never extract/compare manually
}
// Usage:
const hash = await hashPassword('MySecurePassword123');
console.log(hash); // "$2b$12$sAlTsAlTsAlT...hashHashHash"
const valid = await verifyPassword('MySecurePassword123', hash);
console.log(valid); // true
const invalid = await verifyPassword('WrongPassword', hash);
console.log(invalid); // false
Node.js: storing user passwords
import bcrypt from 'bcrypt';
import { db } from './db.js';
const SALT_ROUNDS = 12;
async function registerUser(email, password) {
// 1. Validate password strength before hashing:
if (password.length < 8) throw new Error('Password too short');
// 2. Hash:
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
// 3. Store ONLY the hash — never the plaintext:
const user = await db.users.create({
email: email.toLowerCase(),
passwordHash,
createdAt: new Date(),
});
return user;
}
async function loginUser(email, password) {
const user = await db.users.findByEmail(email.toLowerCase());
// ✅ Always compare even if user not found (prevent timing attacks):
const dummyHash = '$2b$12$invalidhashfortimingattackprevention...';
const hash = user?.passwordHash ?? dummyHash;
const valid = await bcrypt.compare(password, hash);
if (!user || !valid) {
throw new Error('Invalid email or password');
}
return user;
}
Python: bcrypt with passlib
pip install passlib[bcrypt]
from passlib.context import CryptContext
# Configure password hashing:
pwd_context = CryptContext(
schemes=['bcrypt'],
deprecated='auto',
bcrypt__rounds=12,
)
def hash_password(password: str) -> str:
"""Hash a password for storage."""
return pwd_context.hash(password)
def verify_password(plaintext: str, hashed: str) -> bool:
"""Verify a password against a stored hash."""
return pwd_context.verify(plaintext, hashed)
def needs_rehash(hashed: str) -> bool:
"""Check if hash should be upgraded (e.g., after increasing rounds)."""
return pwd_context.needs_update(hashed)
# Usage:
stored_hash = hash_password("MyPassword123")
assert verify_password("MyPassword123", stored_hash) # True
assert not verify_password("WrongPassword", stored_hash) # False
Choosing the right cost factor
| Cost factor | Approx. time/hash | Notes |
|---|---|---|
| 10 | ~100ms | Minimum recommended |
| 12 | ~250ms | Good default (2026) |
| 14 | ~1 second | High-security applications |
| 16 | ~4 seconds | May time out web requests |
Rule of thumb: Choose the highest factor that completes in ≤ 300ms on your server.
// Benchmark on your server:
const start = Date.now();
await bcrypt.hash('test', 12);
console.log(`Cost 12: ${Date.now() - start}ms`);
bcrypt vs Argon2
Argon2 (winner of the 2015 Password Hashing Competition) is generally preferred for new applications:
npm install argon2
import argon2 from 'argon2';
// Hash:
const hash = await argon2.hash('password', {
type: argon2.argon2id, // argon2id recommended
memoryCost: 65536, // 64 MB memory usage
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel threads
});
// Verify:
const valid = await argon2.verify(hash, 'password');
Use bcrypt if you need broad platform support (many languages have well-tested bcrypt libraries). Use Argon2id for new applications where you control the full stack.
Related tools
- Hash Generator — generate and test hashes
- Password Generator — generate strong passwords
- JWT Decoder — inspect authentication tokens
Related posts
- MD5 Is Dead. Use These Instead. — MD5 was broken in 2004 and is trivially cracked for passwords. Here's what to us…
- File Integrity Verification with Checksums — SHA-256 and MD5 — Verify file integrity using SHA-256 and MD5 checksums. Learn how to generate and…
- Hash Functions Comparison — MD5, SHA-1, SHA-256, bcrypt, Argon2 — Hash functions have different speed, output size, and security properties. MD5 a…
- HMAC Authentication — Signing API Requests with Secret Keys — HMAC (Hash-based Message Authentication Code) signs API requests with a shared s…
- Password Generator — How to Build a Secure Random Password Generator — Build a cryptographically secure password generator in JavaScript and Python. Le…
Related tool
Generate MD5, SHA-1, SHA-256, and SHA-512 hashes client-side.
Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.