TL;DR
bcrypt is the industry standard for password hashing. It is intentionally slow, automatically salted, and configurable via a cost factor (work factor / salt rounds). Use a cost factor of 10โ12 for most applications (target 100โ500ms per hash on your hardware). Always use your library's built-in compare() function to verify passwords โ never re-hash and compare strings. For new projects in 2026, consider Argon2id as OWASP's first recommendation, but bcrypt at costย 12 remains safe and battle-tested. You can generate and verify bcrypt hashes online instantly with our free tool.
Key Takeaways
- Never use MD5, SHA-1, or plain SHA-256 to hash passwords โ they are far too fast for attackers to brute-force.
- bcrypt's cost factor doubles the computation time with each increment: cost 10 = 1,024 rounds; cost 12 = 4,096 rounds.
- bcrypt automatically generates and embeds a unique 128-bit salt into every hash, defeating rainbow table attacks.
- A bcrypt hash is always a 60-character string starting with
$2b$(or$2a$) encoding algorithm, cost, salt, and digest. - bcrypt silently truncates passwords at 72 bytes โ mitigate by pre-hashing very long passwords with SHA-256 first.
- Argon2id is the OWASP 2023 first recommendation; bcrypt at cost 12 is acceptable; scrypt is a good middle ground.
- Use
bcrypt.compare()(Node.js),bcrypt.checkpw()(Python), orpassword_verify()(PHP) โ all perform constant-time comparison. - OWASP minimum: cost factor 10 for bcrypt; recommended: 12 for higher security contexts.
What Is bcrypt? The Password Hashing Algorithm Explained
bcrypt is a password hashing function created by Niels Provos and David Mazieres and presented at USENIX 1999. Unlike general-purpose cryptographic hash functions like SHA-256 or MD5, bcrypt is specifically designed for one job: making it as expensive as possible for an attacker to guess passwords in a database breach.
The fundamental problem with hashing passwords with fast algorithms is that modern GPUs can compute billions of SHA-256 hashes per second. If an attacker obtains a database of SHA-256-hashed passwords, they can try every word in a dictionary, every common password variation, and every short combination of characters in seconds or minutes. bcrypt solves this by being intentionally slow: a well-configured bcrypt hash takes 100 to 500 milliseconds to compute, reducing an attacker's throughput from billions per second to a few hundred per second.
bcrypt is built on top of the Blowfish cipher's key scheduling algorithm. The key insight is that Blowfish's key setup (called Eksblowfish) is designed to be expensive: it iterates many rounds of key expansion. bcrypt exposes this cost as the configurable work factor (cost factor), allowing developers to tune the algorithm's speed to match hardware advances over time.
You can immediately try generating bcrypt hashes with our free Bcrypt Generator Online tool, which lets you set the cost factor and verify existing hashes in the browser.
The bcrypt Hash Format Explained
Every bcrypt hash is a 60-character string with a specific structure. Understanding this format helps when debugging, migrating systems, or validating hashes. Here is an example hash for the password hunter2 with cost factor 12:
$2b$12$EXAMPLEsalthere22charsXXhashhere31charsXXXXXXXXXXXXXThe format breaks down as follows:
| Segment | Length | Example | Meaning |
|---|---|---|---|
| Algorithm prefix | 4 chars | $2b$ | bcrypt version ($2b$ is preferred; $2a$ is older; $2y$ is PHP-specific) |
| Cost factor | 2 chars + $ | 12$ | Work factor (2^12 = 4,096 iterations); range is typically 4โ31 |
| Salt | 22 chars | EXAMPLEsalthere22chars | 128-bit random salt encoded in bcrypt's modified Base64 alphabet |
| Hash digest | 31 chars | XXhashhere31charsXXXXXXXXXXXXX | 184-bit (23 bytes) of the actual hash, also in bcrypt's Base64 |
The total is 60 characters (4 + 2 + 1 + 22 + 31). Because the salt is embedded in the hash string, a bcrypt library can verify a password with only two inputs: the plaintext password and the stored hash string. It reads the salt and cost factor from the hash prefix automatically.
Note on versions: $2a$ had a bug in some PHP implementations with certain Unicode characters. $2b$ was introduced to mark the fixed version. $2y$ is PHP's own notation for the same fix. All modern bcrypt libraries use $2b$, and most will accept and correctly verify $2a$ hashes as well.
Understanding the Cost Factor (Work Factor / Salt Rounds)
The cost factor (sometimes called work factor or salt rounds in Node.js bcryptjs documentation) is the single most important parameter you control when using bcrypt. It is an integer, typically between 10 and 14 for production applications.
The relationship between cost factor and computation time is exponential: each increase of 1 doubles the number of iterations performed, which roughly doubles the time required. The formula is:
iterations = 2^cost_factor
cost 10 โ 2^10 = 1,024 iterations
cost 11 โ 2^11 = 2,048 iterations
cost 12 โ 2^12 = 4,096 iterations (OWASP recommended)
cost 13 โ 2^13 = 8,192 iterations
cost 14 โ 2^14 = 16,384 iterationsHere is a real-world benchmark on a typical modern server CPU (approximate values; your hardware will vary):
| Cost Factor | Iterations | ~Time (server) | Attacker hashes/sec (GPU) | Use Case |
|---|---|---|---|---|
| 8 | 256 | ~5 ms | ~200,000/s | Too weak โ do not use in production |
| 10 | 1,024 | ~100 ms | ~50,000/s | OWASP minimum โ acceptable |
| 12 | 4,096 | ~300 ms | ~12,000/s | OWASP recommended โ use this |
| 13 | 8,192 | ~600 ms | ~6,000/s | High security, may feel slow for web login |
| 14 | 16,384 | ~1.2 s | ~3,000/s | Very high security (banking, healthcare) |
The key insight: even at cost factor 12, an attacker with a powerful GPU cluster can only attempt roughly 12,000 bcrypt hashes per second. Cracking a randomly generated 8-character alphanumeric password (62^8 โ 218 trillion combinations) at 12,000 hashes/second would take approximately 578 years. This is why bcrypt is so effective.
The golden rule: benchmark bcrypt hashing on your production server hardware and select the highest cost factor that keeps per-hash time under 500ms for interactive login. Revisit and increase the cost factor every 1โ2 years as hardware improves. When you increase the cost factor, you only need to re-hash passwords as users log in (the old hashes remain valid until then).
How bcrypt Salting Works
A salt is a random value added to the password before hashing to ensure that identical passwords produce different hashes. This defeats rainbow table attacks (precomputed lookup tables of common password hashes) and ensures that if two users have the same password, their hashes will be completely different.
The critical advantage of bcrypt over naive salting approaches is that bcrypt generates, stores, and manages the salt automatically. You do not need to separately generate, store, or manage salts in your database โ the library handles it all. The 128-bit (16-byte) cryptographically random salt is generated fresh for each call to the hash function and embedded directly into the 60-character output string.
Here is what this means in practice:
// Same password, called twice โ different hashes (different random salts)
bcrypt.hash("mysecretpassword", 12)
// โ "$2b$12$AbCdEfGhIjKlMnOpQrStUu8xY3Z..."
bcrypt.hash("mysecretpassword", 12)
// โ "$2b$12$XyZwVuTsRqPoNmLkJiHgFf7hQ1M..."
// Verification still works because the salt is IN the hash string
bcrypt.compare("mysecretpassword", "$2b$12$AbCdEfGhIjKlMnOpQrStUu8xY3Z...")
// โ trueYou only need one column in your database to store the complete bcrypt hash (60 characters, a VARCHAR(60) or CHAR(60) is sufficient). The salt column approach used with older password hashing schemes is unnecessary with bcrypt.
How to Use bcrypt in Node.js
Node.js has two main bcrypt libraries: bcrypt (native bindings, faster) and bcryptjs (pure JavaScript, no native dependencies). For most applications, both work identically from an API perspective. Use bcrypt for performance-critical applications; use bcryptjs when native bindings are problematic (serverless, containers, cross-compilation).
Installation
# Option 1: native bindings (faster, requires build tools)
npm install bcrypt
npm install --save-dev @types/bcrypt # TypeScript
# Option 2: pure JavaScript (no native deps)
npm install bcryptjs
npm install --save-dev @types/bcryptjs # TypeScriptHashing a Password (Async)
import bcrypt from 'bcrypt';
// or: import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12; // OWASP recommended cost factor
/**
* Hash a plain-text password
*/
async function hashPassword(plainPassword: string): Promise<string> {
// bcrypt.hash() generates a random salt internally
const hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);
return hash;
// Returns something like:
// "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"
}
/**
* Verify a password against a stored hash
*/
async function verifyPassword(
plainPassword: string,
storedHash: string
): Promise<boolean> {
// bcrypt.compare() extracts the salt from storedHash automatically
// Uses constant-time comparison to prevent timing attacks
const isMatch = await bcrypt.compare(plainPassword, storedHash);
return isMatch;
}
// Usage example
const password = 'my_secure_password_123!';
const hash = await hashPassword(password);
console.log('Hash:', hash);
// Hash: $2b$12$...
const isValid = await verifyPassword(password, hash);
console.log('Valid:', isValid); // true
const isInvalid = await verifyPassword('wrong_password', hash);
console.log('Invalid:', isInvalid); // falseSync vs Async API
import bcrypt from 'bcrypt';
// โ
PREFER: Async API (non-blocking, use in web servers)
const hashAsync = await bcrypt.hash('password', 12);
const validAsync = await bcrypt.compare('password', hashAsync);
// โ ๏ธ Use with caution: Sync API blocks the event loop!
// Only acceptable in scripts, CLI tools, or tests
const hashSync = bcrypt.hashSync('password', 12);
const validSync = bcrypt.compareSync('password', hashSync);
// In Express.js / Fastify routes, ALWAYS use the async API
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// Validate password strength first
if (password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
const hash = await bcrypt.hash(password, 12);
await db.users.create({ username, passwordHash: hash });
res.json({ success: true });
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await db.users.findOne({ username });
if (!user) {
// Still run bcrypt to prevent timing-based username enumeration
await bcrypt.hash(password, 12);
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateJWT(user);
res.json({ token });
});Handling the 72-Byte Limit in Node.js
import bcrypt from 'bcrypt';
import crypto from 'crypto';
/**
* Pre-hash with SHA-256 to handle passwords longer than 72 bytes.
* The SHA-256 hex output (64 chars) is always within bcrypt's 72-byte limit.
*/
function prehashPassword(password: string): string {
return crypto
.createHash('sha256')
.update(password)
.digest('hex'); // 64 hex chars, always < 72 bytes
}
async function hashPasswordSafe(password: string): Promise<string> {
const prehashed = prehashPassword(password);
return bcrypt.hash(prehashed, 12);
}
async function verifyPasswordSafe(
password: string,
storedHash: string
): Promise<boolean> {
const prehashed = prehashPassword(password);
return bcrypt.compare(prehashed, storedHash);
}
// IMPORTANT: If you switch to this approach on an existing system,
// ALL existing users must reset their passwords (old hashes are incompatible).
// Use this pattern only for new systems or after a coordinated migration.Upgrading Cost Factor on Next Login
import bcrypt from 'bcrypt';
const CURRENT_COST = 12;
async function loginWithCostUpgrade(
password: string,
storedHash: string,
userId: string
): Promise<boolean> {
const isValid = await bcrypt.compare(password, storedHash);
if (!isValid) return false;
// Check if the stored hash uses an outdated cost factor
const hashCost = parseInt(storedHash.split('$')[2], 10);
if (hashCost < CURRENT_COST) {
// Re-hash with the new cost factor transparently
const newHash = await bcrypt.hash(password, CURRENT_COST);
await db.users.update({ id: userId, passwordHash: newHash });
console.log(`Upgraded password hash for user ${userId}: cost ${hashCost} โ ${CURRENT_COST}`);
}
return true;
}How to Use bcrypt in Python
Python's primary bcrypt library is the bcrypt package, which wraps OpenBSD's native bcrypt C implementation. It is fast, well-maintained, and straightforward to use.
Installation
pip install bcrypt
# For Django projects, use Django's built-in hasher instead:
# PASSWORD_HASHERS in settings.py
# Django supports bcrypt natively when bcrypt is installedHashing and Verifying Passwords
import bcrypt
COST_FACTOR = 12 # OWASP recommended
def hash_password(plain_password: str) -> bytes:
"""Hash a password with bcrypt. Returns the hash as bytes."""
# Encode the string to bytes first (bcrypt requires bytes)
password_bytes = plain_password.encode('utf-8')
# bcrypt.gensalt() generates a random 128-bit salt with the given rounds
salt = bcrypt.gensalt(rounds=COST_FACTOR)
# bcrypt.hashpw() computes the hash
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed
# Returns: b'$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW'
def verify_password(plain_password: str, stored_hash: bytes) -> bool:
"""Verify a password against a stored bcrypt hash."""
password_bytes = plain_password.encode('utf-8')
# checkpw() handles salt extraction and constant-time comparison
return bcrypt.checkpw(password_bytes, stored_hash)
# Usage
password = "my_secure_password_123!"
hashed = hash_password(password)
print(f"Hash: {hashed}")
# b'$2b$12$...'
print(verify_password(password, hashed)) # True
print(verify_password("wrong_password", hashed)) # FalseStoring bcrypt Hashes in a Database (Python)
import bcrypt
import sqlite3
# bcrypt returns bytes; store as TEXT in the database (decode to str)
def hash_password_for_db(plain_password: str) -> str:
hashed_bytes = bcrypt.hashpw(
plain_password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
)
return hashed_bytes.decode('utf-8') # Store as string
def verify_from_db(plain_password: str, stored_hash_str: str) -> bool:
# Re-encode the stored string to bytes for checkpw()
return bcrypt.checkpw(
plain_password.encode('utf-8'),
stored_hash_str.encode('utf-8')
)
# Database example (SQLite)
conn = sqlite3.connect('users.db')
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL -- VARCHAR(60) is sufficient
)
''')
def register_user(username: str, password: str):
hash_str = hash_password_for_db(password)
conn.execute(
'INSERT INTO users (username, password_hash) VALUES (?, ?)',
(username, hash_str)
)
conn.commit()
def login_user(username: str, password: str) -> bool:
row = conn.execute(
'SELECT password_hash FROM users WHERE username = ?',
(username,)
).fetchone()
if not row:
# Prevent timing-based username enumeration with a dummy comparison
bcrypt.checkpw(
password.encode('utf-8'),
bcrypt.hashpw(b'dummy', bcrypt.gensalt(rounds=4))
)
return False
return verify_from_db(password, row[0])bcrypt with Django
# 1. Install: pip install django[bcrypt] or pip install bcrypt
# 2. In settings.py, add BCryptSHA256PasswordHasher FIRST in the list:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # fallback for old hashes
]
# 3. Django handles everything automatically:
from django.contrib.auth import authenticate, get_user_model
User = get_user_model()
# Creating a user โ Django hashes the password automatically
user = User.objects.create_user(username='alice', password='SecurePass123!')
# user.password now stores: bcrypt$$2b$12$...
# Verifying โ Django uses checkpw() internally
auth_user = authenticate(username='alice', password='SecurePass123!')
# Returns the user object if valid, None if invalid
# Django's BCryptSHA256PasswordHasher wraps the password with HMAC-SHA256
# before passing to bcrypt, which both handles the 72-byte limit AND
# adds an extra layer of keyed hashing using Django's SECRET_KEYHow to Use bcrypt in PHP
PHP has included bcrypt support natively since PHP 5.5 through the password_hash() and password_verify() functions. These are the recommended functions โ you should never use the older crypt() function directly.
Basic Hashing and Verification
<?php
/**
* Hash a password using bcrypt via PHP's password_hash()
*/
function hashPassword(string $password): string {
$options = [
'cost' => 12, // OWASP recommended cost factor
];
$hash = password_hash($password, PASSWORD_BCRYPT, $options);
if ($hash === false) {
throw new RuntimeException('Failed to hash password');
}
return $hash;
// Returns: "$2y$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"
// Note: PHP uses $2y$ prefix (equivalent to $2b$ from other implementations)
}
/**
* Verify a password against a stored bcrypt hash
*/
function verifyPassword(string $password, string $storedHash): bool {
return password_verify($password, $storedHash);
// Returns true/false; uses constant-time comparison internally
}
// Usage
$password = 'my_secure_password_123!';
$hash = hashPassword($password);
echo "Hash: $hash
";
var_dump(verifyPassword($password, $hash)); // bool(true)
var_dump(verifyPassword('wrong_password', $hash)); // bool(false)
/**
* Check if a hash needs to be re-hashed with updated options
* Use this on each successful login to upgrade old hashes
*/
function rehashIfNeeded(string $password, string $storedHash, PDO $db, int $userId): void {
$options = ['cost' => 12];
if (password_needs_rehash($storedHash, PASSWORD_BCRYPT, $options)) {
$newHash = password_hash($password, PASSWORD_BCRYPT, $options);
$stmt = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
$stmt->execute([$newHash, $userId]);
}
}Complete PHP Login System
<?php
// config.php
define('BCRYPT_COST', 12);
define('MIN_PASSWORD_LENGTH', 8);
class UserAuth {
private PDO $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function register(string $username, string $password): array {
// Validate input
if (strlen($password) < MIN_PASSWORD_LENGTH) {
return ['success' => false, 'error' => 'Password too short'];
}
if (strlen($username) < 3) {
return ['success' => false, 'error' => 'Username too short'];
}
// Check if username already exists
$stmt = $this->db->prepare('SELECT id FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
return ['success' => false, 'error' => 'Username already taken'];
}
// Hash password with bcrypt
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]);
// Store user
$stmt = $this->db->prepare(
'INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, NOW())'
);
$stmt->execute([$username, $hash]);
return ['success' => true, 'user_id' => $this->db->lastInsertId()];
}
public function login(string $username, string $password): array {
$stmt = $this->db->prepare(
'SELECT id, password_hash FROM users WHERE username = ?'
);
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
// Prevent timing attack: run a dummy hash even when user doesn't exist
password_verify($password, '$2y$12$invalid.hash.for.timing.prevention.only');
return ['success' => false, 'error' => 'Invalid credentials'];
}
if (!password_verify($password, $user['password_hash'])) {
return ['success' => false, 'error' => 'Invalid credentials'];
}
// Upgrade hash if cost factor has changed
if (password_needs_rehash($user['password_hash'], PASSWORD_BCRYPT, ['cost' => BCRYPT_COST])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]);
$update = $this->db->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
$update->execute([$newHash, $user['id']]);
}
return ['success' => true, 'user_id' => $user['id']];
}
}bcrypt vs Argon2 vs scrypt: Which Should You Use in 2026?
Choosing the right password hashing algorithm in 2026 depends on your language ecosystem, infrastructure constraints, and security requirements. Here is a detailed comparison of all three major algorithms:
| Property | bcrypt | Argon2id | scrypt |
|---|---|---|---|
| Year created | 1999 | 2015 | 2009 |
| OWASP 2023 recommendation | Acceptable (2nd choice) | First choice (Argon2id) | Acceptable (3rd choice) |
| Memory-hardness | None (CPU-only) | Yes (configurable memory) | Yes (configurable memory) |
| GPU resistance | Moderate (CPU-heavy ops) | Excellent | Excellent |
| ASIC resistance | Low | High | Moderate |
| Password length limit | 72 bytes (silent truncation) | Unlimited | Unlimited |
| Output length | Always 60 chars | Configurable | Configurable |
| Tuning parameters | 1 (cost factor) | 3 (time, memory, parallelism) | 3 (N, r, p) |
| Configuration difficulty | Simple (just cost) | Moderate | Complex (easy to misconfigure) |
| Library availability | Excellent (all ecosystems) | Good (growing rapidly) | Moderate |
| Built-in language support | PHP, Ruby, many frameworks | Python (passlib), PHP 8.2+ | Python (hashlib), Node.js (crypto) |
| OWASP min params (2023) | cost โฅ 10 | m=19 MiB, t=2, p=1 | N=2^17, r=8, p=1 |
| Best for | Existing systems, PHP/Ruby apps, maximum compatibility | New applications, maximum security | Systems where Argon2 is unavailable but memory-hardness is needed |
Recommendation for 2026: Use Argon2id for new applications if your language/framework supports it well. Use bcrypt at cost 12 if you are maintaining an existing system or if Argon2id library support is limited in your ecosystem. Avoid scrypt unless you have a specific reason โ its configuration is error-prone and Argon2id supersedes it in every respect.
Note: you can also compare raw hash algorithms using our Hash Generator Online to understand how MD5, SHA-256, and SHA-512 differ from password-hashing algorithms.
Argon2 in Node.js: A Modern Alternative
If you are starting a new Node.js application in 2026 and want to use Argon2id (the OWASP first recommendation), here is how:
npm install argon2import argon2 from 'argon2';
// OWASP Argon2id minimum settings (2023):
// Memory: 19 MiB (19456 KiB), Time: 2 iterations, Parallelism: 1
const ARGON2_OPTIONS = {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB in KiB
timeCost: 2, // 2 iterations
parallelism: 1,
};
async function hashPasswordArgon2(password: string): Promise<string> {
return argon2.hash(password, ARGON2_OPTIONS);
// Returns: "$argon2id$v=19$m=19456,t=2,p=1$..."
}
async function verifyPasswordArgon2(
password: string,
hash: string
): Promise<boolean> {
return argon2.verify(hash, password);
}
// Argon2id has no password length limit and is more GPU-resistant than bcrypt
// It is the better choice for new applications in 2026Common Bcrypt Mistakes and How to Avoid Them
Mistake 1: Comparing Hash Strings Directly
import bcrypt from 'bcrypt';
// โ WRONG: Never do this
const hash1 = await bcrypt.hash('password', 12);
const hash2 = await bcrypt.hash('password', 12);
// hash1 !== hash2 because they have different random salts!
if (hash1 === hash2) { /* This will NEVER be true */ }
// โ ALSO WRONG: Never hash then compare
const inputHash = await bcrypt.hash(userInput, 12);
if (inputHash === storedHash) { /* Different salts โ never matches */ }
// โ
CORRECT: Always use compare()
const isValid = await bcrypt.compare(userInput, storedHash);Mistake 2: Using a Too-Low Cost Factor
// โ WRONG: Default cost of 10 is the MINIMUM, not a good choice
const hash = await bcrypt.hash(password, 10); // Barely acceptable
// โ VERY WRONG: Cost of 4 or 5 is dangerously weak
const weakHash = await bcrypt.hash(password, 5); // Never in production!
// โ
CORRECT: Use at least 12 for meaningful security
const secureHash = await bcrypt.hash(password, 12); // OWASP recommended
// โ
BEST: Benchmark on your hardware and target 100-500ms
import { performance } from 'perf_hooks';
for (let cost = 10; cost <= 14; cost++) {
const start = performance.now();
await bcrypt.hash('benchmark_password', cost);
const ms = performance.now() - start;
console.log(`Cost ${cost}: ${ms.toFixed(0)}ms`);
}
// Pick the highest cost that stays under 500ms on your serverMistake 3: Blocking the Event Loop with Sync Hashing
import bcrypt from 'bcrypt';
// โ WRONG: bcrypt.hashSync() blocks Node.js event loop for ~300ms!
// During this time, your server cannot handle ANY other requests.
app.post('/login', (req, res) => {
const hash = bcrypt.hashSync(req.body.password, 12); // BLOCKS EVERYTHING
// ...
});
// โ
CORRECT: Always use the async API in request handlers
app.post('/login', async (req, res) => {
try {
const hash = await bcrypt.hash(req.body.password, 12); // Non-blocking
// ...
} catch (err) {
res.status(500).json({ error: 'Internal error' });
}
});
// Note: Even the async API uses a thread pool (via libuv) to offload CPU work,
// so it doesn't truly block Node's event loop.Mistake 4: Not Handling the 72-Byte Limit
import bcrypt from 'bcrypt';
// โ POTENTIAL ISSUE: Passwords longer than 72 bytes are silently truncated
// "A_very_long_password_that_is_exactly_73_bytes_long_abc" and
// "A_very_long_password_that_is_exactly_73_bytes_long_xyz" hash identically!
// Test if a string exceeds 72 bytes (UTF-8 characters can be multibyte!)
function exceedsBcryptLimit(password: string): boolean {
return Buffer.byteLength(password, 'utf8') > 72;
}
// โ
OPTION 1: Reject passwords > 72 bytes
if (exceedsBcryptLimit(password)) {
throw new Error('Password too long (maximum 72 bytes for bcrypt)');
}
// โ
OPTION 2: Pre-hash with SHA-256 (converts any length to 64 hex chars)
import crypto from 'crypto';
function prehash(password: string): string {
return crypto.createHash('sha256').update(password).digest('hex');
// Always returns a 64-char hex string โ safely within 72-byte limit
}
const safeHash = await bcrypt.hash(prehash(password), 12);
const isValid = await bcrypt.compare(prehash(userInput), safeHash);Mistake 5: Username Enumeration via Timing Differences
import bcrypt from 'bcrypt';
// โ WRONG: Returns immediately for non-existent users
// An attacker can time the response to determine if a username exists:
// - Existing user: ~300ms (bcrypt runs)
// - Non-existent user: ~1ms (immediate return)
app.post('/login', async (req, res) => {
const user = await db.findUser(req.body.username);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' }); // Too fast!
}
const valid = await bcrypt.compare(req.body.password, user.passwordHash);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
// ...
});
// โ
CORRECT: Always run bcrypt even for non-existent users
const DUMMY_HASH = '$2b$12$invalidhashfortimingatk1234567890123456789012345';
app.post('/login', async (req, res) => {
const user = await db.findUser(req.body.username);
const hashToCheck = user ? user.passwordHash : DUMMY_HASH;
// Always runs bcrypt โ takes ~300ms regardless of whether user exists
const isValid = user && await bcrypt.compare(req.body.password, hashToCheck);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// ...
});Mistake 6: Storing Plaintext Passwords in Logs
// โ CATASTROPHIC: Never log passwords
app.post('/login', async (req, res) => {
console.log('Login attempt:', req.body); // LOGS THE PASSWORD!
console.log('Password:', req.body.password); // NEVER DO THIS
});
// โ
CORRECT: Log only what you need, never the password
app.post('/login', async (req, res) => {
const { username, password } = req.body;
console.log('Login attempt for user:', username); // Safe
const isValid = await bcrypt.compare(password, storedHash);
console.log('Login result:', isValid ? 'success' : 'failure'); // Safe
});When to Use bcrypt vs When Not To
Use bcrypt (or Argon2id) for:
- User account passwords โ this is bcrypt's primary use case
- API keys stored in databases โ if you store user-provided API keys, hash them like passwords
- Security tokens that must be looked up โ password reset tokens, magic login links
- PIN codes โ 4โ6 digit PINs have tiny search spaces; bcrypt's slowness is critical
- Security questions (though security questions are generally poor practice)
Do NOT use bcrypt for:
- General-purpose data hashing โ use SHA-256 for file integrity, ETags, cache keys
- HMAC / API request signing โ use HMAC-SHA256 instead (see our HMAC Generator)
- Large data hashing โ bcrypt has a 72-byte limit; use SHA-256 or SHA-3 for files
- High-frequency hashing โ bcrypt is too slow for use in caching layers or stream processing
- Symmetric encryption โ bcrypt is a hash function, not encryption; use AES-256-GCM
- Key derivation for encryption โ use PBKDF2, scrypt, or Argon2 with a proper KDF context
- Token signing โ use HMAC-SHA256 or RSA/ECDSA for JWTs
Password Strength Still Matters with bcrypt
bcrypt dramatically slows down attackers, but it is not a substitute for strong passwords. If a user chooses the password password1, an attacker who obtains the bcrypt hash can still crack it because the search space is so small. Here is the math:
With bcrypt at cost 12, an attacker can attempt roughly 12,000 hashes per second on a modern GPU. The most common 10 million passwords can be tried in approximately 14 minutes. A dictionary of 1 billion common passwords/patterns could be exhausted in about 23 hours.
A randomly generated 8-character password using lowercase only (26^8 โ 208 billion combinations) would take about 200 days at 12,000 hashes/second. A 12-character mixed-case alphanumeric password (62^12 โ 3.2 x 10^21) would take over 8 trillion years.
This is why you need both bcrypt (or Argon2id) AND password strength requirements. Use our Password Generator Online to create strong, random passwords, and enforce minimum length and complexity requirements in your application.
Migrating from MD5 / SHA-256 to bcrypt
Many legacy applications store passwords as unsalted MD5 or SHA-256 hashes. Migrating these to bcrypt without forcing all users to reset their passwords requires a careful approach:
Strategy 1: Hash-on-Login Migration
import bcrypt from 'bcrypt';
import crypto from 'crypto';
// Add a column: hash_version ENUM('md5', 'sha256', 'bcrypt') NOT NULL DEFAULT 'md5'
async function legacyLogin(
password: string,
user: { id: string; passwordHash: string; hashVersion: string }
): Promise<boolean> {
let isValid = false;
if (user.hashVersion === 'bcrypt') {
// Modern path: direct bcrypt compare
isValid = await bcrypt.compare(password, user.passwordHash);
} else if (user.hashVersion === 'sha256') {
// Legacy SHA-256: compare then upgrade
const sha256 = crypto.createHash('sha256').update(password).digest('hex');
isValid = (sha256 === user.passwordHash);
} else if (user.hashVersion === 'md5') {
// Legacy MD5: compare then upgrade
const md5 = crypto.createHash('md5').update(password).digest('hex');
isValid = (md5 === user.passwordHash);
}
if (isValid && user.hashVersion !== 'bcrypt') {
// Silently upgrade to bcrypt on next successful login
const bcryptHash = await bcrypt.hash(password, 12);
await db.users.update({
id: user.id,
passwordHash: bcryptHash,
hashVersion: 'bcrypt',
});
}
return isValid;
}Strategy 2: Double-Hash Bridge (No Migration Column Needed)
import bcrypt from 'bcrypt';
import crypto from 'crypto';
// Wrap ALL existing MD5 hashes in bcrypt immediately (no user action required)
// Run this migration script ONCE against your database:
async function migrateMd5ToBcrypt(userId: string, existingMd5Hash: string) {
// The MD5 hash becomes the "password" that bcrypt hashes
// (md5 output is 32 hex chars, safely within bcrypt's 72-byte limit)
const bcryptWrapped = await bcrypt.hash(existingMd5Hash, 12);
await db.execute(
'UPDATE users SET password_hash = ?, hash_version = ? WHERE id = ?',
[bcryptWrapped, 'bcrypt_over_md5', userId]
);
}
// Login verification for migrated users:
async function verifyMigratedUser(
password: string,
storedBcryptHash: string
): Promise<boolean> {
// Re-compute the MD5 hash of the password, then bcrypt.compare
const md5 = crypto.createHash('md5').update(password).digest('hex');
return bcrypt.compare(md5, storedBcryptHash);
// On next login, you can further migrate to pure bcrypt
}How to Use the Bcrypt Generator Online Tool
Our free Bcrypt Generator Online tool makes it easy to generate and verify bcrypt hashes directly in your browser without installing any software. Here is how to use it:
- Generate a hash: Enter the password or string you want to hash in the input field. Select your desired cost factor (10, 11, or 12 are most common). Click Generate Hash. The tool computes the bcrypt hash entirely in your browser using the WebAssembly bcrypt implementation โ your password never leaves your device.
- Verify a hash: Paste an existing bcrypt hash into the Hash field, enter the password to test in the Password field, and click Verify. The tool will tell you whether the password matches the hash. This is useful for testing your application's hash generation, debugging authentication issues, or verifying hashes from external systems.
- Experiment with cost factors: Try generating the same password at cost 10, 11, and 12 to see how the hash changes (it will, due to the different salt and iterations) and observe the time difference in your browser.
Use this tool alongside our Hash Generator (for SHA-256, MD5, SHA-512) and HMAC Generator (for API signing) to cover all your cryptographic hashing needs.
bcrypt in Other Languages and Frameworks
Java / Spring Boot
// Maven dependency:
// <dependency>
// <groupId>org.springframework.security</groupId>
// <artifactId>spring-security-crypto</artifactId>
// </dependency>
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
// Cost factor 12 (Spring's default is 10; always specify explicitly)
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String rawPassword = "my_secure_password";
String hashedPassword = encoder.encode(rawPassword);
// "$2a$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"
boolean matches = encoder.matches(rawPassword, hashedPassword);
// true
// In Spring Security configuration:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
// Spring Security uses this automatically for:
// userDetailsService.loadUserByUsername() โ password check
// authenticationManager.authenticate() โ loginRuby on Rails
# Gemfile
gem 'bcrypt', '~> 3.1.7'
# In your User model (has_secure_password uses bcrypt automatically)
class User < ApplicationRecord
has_secure_password # adds password_digest column, authenticate() method
# Configure cost (default is 12 in bcrypt gem)
# In config/environments/production.rb:
# BCrypt::Engine.cost = 12
end
# Usage:
user = User.create(password: 'secure_password_123!')
# user.password_digest = "$2a$12$..."
user.authenticate('secure_password_123!') # Returns user object (truthy)
user.authenticate('wrong_password') # Returns false
# Direct bcrypt usage (without has_secure_password):
require 'bcrypt'
password = BCrypt::Password.create('my_password', cost: 12)
# "$2a$12$..."
password.is_password?('my_password') # true
password.is_password?('wrong_pass') # falseGo
// go get golang.org/x/crypto/bcrypt
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func hashPassword(password string) (string, error) {
// bcrypt.DefaultCost is 10; use 12 for OWASP recommendation
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", err
}
return string(bytes), nil
}
func verifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil // nil means password matches
}
func main() {
hash, err := hashPassword("my_secure_password")
if err != nil {
panic(err)
}
fmt.Println("Hash:", hash)
fmt.Println("Valid:", verifyPassword("my_secure_password", hash)) // true
fmt.Println("Invalid:", verifyPassword("wrong_password", hash)) // false
// Get cost factor from existing hash
cost, err := bcrypt.Cost([]byte(hash))
if err != nil {
panic(err)
}
fmt.Println("Cost factor:", cost) // 12
}Database Schema Best Practices for bcrypt
-- PostgreSQL / MySQL: users table with bcrypt
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
-- CHAR(60) is ideal: bcrypt output is always exactly 60 characters
password_hash CHAR(60) NOT NULL,
-- Track hash version to support future algorithm upgrades
hash_version VARCHAR(20) NOT NULL DEFAULT 'bcrypt_12',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login TIMESTAMPTZ,
login_attempts INT NOT NULL DEFAULT 0,
locked_until TIMESTAMPTZ
);
-- Index for login lookups
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- IMPORTANT: Never store the plaintext password.
-- Never index password_hash (it never needs to be queried directly).
-- Always query by username/email to find the user, THEN call bcrypt.compare().Rate Limiting and Brute-Force Protection
bcrypt makes individual hash attempts expensive, but it does not protect against distributed brute-force attacks where an attacker tries millions of passwords from thousands of different IP addresses. You need additional layers of protection:
import bcrypt from 'bcrypt';
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// 1. Rate limiting per IP address
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Max 10 login attempts per IP per 15 min
message: 'Too many login attempts, please try again later',
store: new RedisStore({ /* redis client */ }), // Use Redis for distributed systems
});
app.post('/login', loginLimiter, async (req, res) => {
const { email, password } = req.body;
// 2. Per-account lockout (database-level)
const user = await db.findUserByEmail(email);
if (!user) {
await bcrypt.hash(password, 12); // Timing protection
return res.status(401).json({ error: 'Invalid credentials' });
}
if (user.lockedUntil && user.lockedUntil > new Date()) {
return res.status(423).json({
error: 'Account temporarily locked',
retryAfter: user.lockedUntil,
});
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
// 3. Increment failed attempt counter and lock if threshold exceeded
const attempts = user.loginAttempts + 1;
const lockUntil = attempts >= 5
? new Date(Date.now() + 15 * 60 * 1000) // Lock for 15 min after 5 failures
: null;
await db.updateUser(user.id, {
loginAttempts: attempts,
lockedUntil: lockUntil,
});
return res.status(401).json({ error: 'Invalid credentials' });
}
// 4. Reset counter on success
await db.updateUser(user.id, {
loginAttempts: 0,
lockedUntil: null,
lastLogin: new Date(),
});
const token = generateJWT(user);
res.json({ token });
});Testing bcrypt in Your Application
import bcrypt from 'bcrypt';
import { describe, it, expect, beforeAll } from 'vitest';
describe('Password hashing', () => {
// โ
Use a LOW cost factor in tests (cost 4 = minimum) to avoid slow tests
// NEVER use cost 4 in production!
const TEST_COST = 4;
it('should hash a password and return a bcrypt string', async () => {
const hash = await bcrypt.hash('test_password', TEST_COST);
expect(hash).toMatch(/^$2b$04$/); // Starts with $2b$04$
expect(hash).toHaveLength(60); // Always 60 chars
});
it('should produce different hashes for the same password', async () => {
const hash1 = await bcrypt.hash('same_password', TEST_COST);
const hash2 = await bcrypt.hash('same_password', TEST_COST);
expect(hash1).not.toBe(hash2); // Different salts
});
it('should verify correct passwords', async () => {
const hash = await bcrypt.hash('correct_password', TEST_COST);
const result = await bcrypt.compare('correct_password', hash);
expect(result).toBe(true);
});
it('should reject wrong passwords', async () => {
const hash = await bcrypt.hash('correct_password', TEST_COST);
const result = await bcrypt.compare('wrong_password', hash);
expect(result).toBe(false);
});
it('should verify a hash from a different cost factor', async () => {
// Hashes generated with cost 10 should still verify correctly
const legacyHash = await bcrypt.hash('my_password', 10);
const result = await bcrypt.compare('my_password', legacyHash);
expect(result).toBe(true);
});
// Tip: Mock bcrypt in integration tests that don't need real hashing
// Use vi.mock('bcrypt', () => ({ hash: vi.fn().mockResolvedValue('$2b$fake...'), compare: vi.fn().mockResolvedValue(true) }))
});Related Tools and Further Reading
bcrypt is one piece of a comprehensive security toolkit. Explore these related tools on DevToolBox:
- Bcrypt Generator Online โ generate and verify bcrypt hashes instantly in your browser.
- Hash Generator โ generate MD5, SHA-1, SHA-256, SHA-384, and SHA-512 hashes. See our Hash Generator Guide for a detailed explanation of each algorithm.
- Password Generator โ generate cryptographically strong random passwords to pair with bcrypt.
- HMAC Generator โ generate HMAC-SHA256 codes for API signing and webhook verification.
Frequently Asked Questions
What is bcrypt and why is it used for password hashing?
bcrypt is a password hashing function designed in 1999 by Niels Provos and David Mazieres. It is based on the Blowfish cipher's key scheduling algorithm and is intentionally designed to be slow and configurable โ properties that are the opposite of what you want in a general-purpose hash function but exactly what you want for passwords. See the full answer above in "What Is bcrypt?"
What cost factor should I use for bcrypt in 2026?
OWASP's 2023 recommendation is a minimum cost factor of 10, with 12 being preferred. Benchmark on your production hardware: pick the highest cost that keeps per-hash time under 500ms for interactive login. Revisit every 1โ2 years as hardware improves.
How do I verify a password against a bcrypt hash?
Use your library's built-in compare function: bcrypt.compare() in Node.js, bcrypt.checkpw() in Python, password_verify() in PHP, BCryptPasswordEncoder.matches() in Java. Never hash the input and compare strings directly โ each bcrypt hash has a unique salt so re-hashing will always produce a different hash.
What is the difference between bcrypt, Argon2, and scrypt?
bcrypt uses CPU time only (no memory-hardness), has a 72-byte password limit, and is simplest to configure. Argon2id adds memory-hardness and parallelism for superior GPU/ASIC resistance, has no password limit, and is OWASP's 2023 first choice. scrypt adds memory-hardness but is harder to configure correctly. See the full comparison table above.
Is bcrypt still safe in 2026?
Yes. No practical attacks against bcrypt's core algorithm have been found in 25+ years. The main concerns are the 72-byte password limit and that Argon2id offers better memory-hardness. For existing applications using bcrypt at cost 12, it remains safe. New applications should consider Argon2id.
Can bcrypt hashes be cracked?
bcrypt hashes cannot be reversed, but they can be cracked by guessing. The high cost factor makes this extremely slow: roughly 12,000 attempts/second at cost 12 on a GPU. Weak or common passwords remain vulnerable even with bcrypt. Strong, random passwords paired with bcrypt at cost 12 are effectively uncrackable with current technology.
Why does bcrypt truncate at 72 bytes?
bcrypt's underlying Blowfish cipher processes the password in 18 four-byte words (72 bytes). Any bytes beyond position 72 are ignored. Mitigate by pre-hashing with SHA-256 (the 64-char hex output is always within the limit) or by using Argon2 which has no limit.
How do I migrate from MD5/SHA-256 to bcrypt?
You cannot reverse old hashes, but you have two options: (1) Hash-on-login: when a user logs in, verify against the old hash, then immediately re-hash and store the bcrypt version; (2) Double-hash bridge: bcrypt-wrap all existing MD5/SHA-256 hashes immediately (the old hash becomes the input to bcrypt), then gradually migrate fully on next login. See the full code examples in the migration section above.