Password hashing is a specific class of hash function — deliberately slow, memory-hard, salted. General-purpose hashes (MD5, SHA-256) are the wrong tool.
The Rule
Use bcrypt or Argon2id. Never MD5, SHA-1, SHA-256, or SHA-512 alone.
Method 1 — bcrypt (Battle-Tested)
Cost factor 12 in 2026 (~250ms per hash on a modern server):
Python:
import bcrypt
password = b"user-password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# Verify:
bcrypt.checkpw(password, hashed) # True
Node.js:
const bcrypt = require('bcrypt');
const hashed = await bcrypt.hash('user-password', 12);
// Verify:
await bcrypt.compare('user-password', hashed); // true
Ruby:
require 'bcrypt'
hashed = BCrypt::Password.create('user-password', cost: 12)
BCrypt::Password.new(hashed) == 'user-password' # true
Output format:
$2b$12$Rn8OqhElVXYs.7g5CD3Ovu8k5UyC.pQxPnr4jZTvC9NcJC/1kHJbG
Method 2 — Argon2id (Modern Recommendation)
Parameters for 2026: memory=64MB (65536 KiB), time=3, parallelism=4.
Python:
from argon2 import PasswordHasher
ph = PasswordHasher(memory_cost=65536, time_cost=3, parallelism=4)
hashed = ph.hash("user-password")
# Verify:
ph.verify(hashed, "user-password") # True
Node.js:
const argon2 = require('argon2');
const hashed = await argon2.hash('user-password', {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4,
});
// Verify:
await argon2.verify(hashed, 'user-password'); // true
Output format:
$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0$hashhashhashhash...
Method 3 — Quick Testing
Use the hash generator — paste a password, get bcrypt or Argon2 output. Useful for testing verification code without writing generation code.
Cost Tuning
Both bcrypt and Argon2 support tunable work factors. Target ~250ms per hash on your production hardware:
- bcrypt — cost 12 in 2026, bump to 13 in ~2 years
- Argon2id — memory=64MB is the safe baseline; bump to 128MB for higher-security systems
Rehash on user login when the cost is out of date.
Salt
Both bcrypt and Argon2 include salt automatically — you don’t need to add it separately. The salt is embedded in the output hash string.
Verification
Never do hash(input) == stored_hash for comparison — use the library’s verify function to prevent timing attacks:
- bcrypt:
bcrypt.checkpw()(Python),bcrypt.compare()(Node) - Argon2:
PasswordHasher.verify()(Python),argon2.verify()(Node)
Common Miss
- Using MD5 / SHA-256 / SHA-512 for passwords — modern GPUs crack billions per second
- Wrapping bcrypt around SHA-256 (
bcrypt(sha256(password))) — adds no security, caps entropy at 256 bits - Storing hashes without cost/parameters — the hash format includes them; keep the format intact
- Truncating passwords at 72 bytes without warning users (bcrypt’s limit) — silent security regression
- Custom “salted SHA-256” schemes — always insecure vs bcrypt/Argon2
Read the bcrypt glossary entry and Argon2 entry for the algorithm details.
Related
Check the bcrypt glossary entry, the Argon2 entry, and see why MD5 is broken for passwords.