Password Hashing Guide: Bcrypt, Argon2 & PBKDF2 Explained
A complete developer's guide to modern password hashing. Learn the differences between Bcrypt, Argon2id, and PBKDF2, and how to choose the right cost factors.
Interactive Password Hash & Verifier
Live Interactive SandboxComputes Bcrypt, Argon2, or PBKDF2 hashes locally using the Web Crypto API. Test the preset input below or customize it before launching into the full workspace.
Password hashing is the foundational layer of application security. Unlike encryption (which is reversible), hashing is a one-way function — you can verify a password against a hash, but you cannot reverse the hash back to the original password.
Choosing the right algorithm and tuning its cost parameters is critical. Get it wrong and an attacker with a stolen database can crack millions of passwords per second on commodity GPUs.
Why Not SHA-256 or MD5?
General-purpose hash functions like SHA-256 and MD5 are designed to be fast. That's the exact opposite of what you want for password storage.
| Algorithm | Hashes/sec (RTX 4090) | Purpose |
|---|---|---|
| MD5 | ~164 billion | Checksums, NOT passwords |
| SHA-256 | ~22 billion | Data integrity, NOT passwords |
| Bcrypt (cost 12) | ~32,000 | ✅ Password storage |
| Argon2id (64MB) | ~1,000 | ✅ Password storage |
The entire point of a password hash function is to be deliberately slow so that brute-force attacks become economically unfeasible.
1. Bcrypt — The Industry Standard
Bcrypt has been the default recommendation since 1999. It uses the Blowfish cipher internally and has a built-in salt.
How Bcrypt Output Looks
$2b$12$LJ3m4ys3YOlTBSVVoJBNMezgqLcJEE3I7mFtHxMUb4uVlvDFqmqy6
│ │ │
│ │ └── 22-char salt + 31-char hash (Base64 encoded)
│ └───── Cost factor (2^12 = 4096 iterations)
└──────── Algorithm identifier ($2b$ = modern Bcrypt)
Bcrypt in Code
# Python (bcrypt library)
import bcrypt
password = b"CorrectHorseBatteryStaple"
salt = bcrypt.gensalt(rounds=12) # Cost factor = 12
hashed = bcrypt.hashpw(password, salt)
# Verify
if bcrypt.checkpw(password, hashed):
print("Password matches")
// Node.js (bcryptjs)
const bcrypt = require('bcryptjs');
const hash = await bcrypt.hash('CorrectHorseBatteryStaple', 12);
const isValid = await bcrypt.compare('CorrectHorseBatteryStaple', hash);
Choosing the Cost Factor
| Cost | Iterations | ~Time (modern CPU) | Use Case |
|---|---|---|---|
| 10 | 1,024 | ~65ms | Minimum acceptable |
| 12 | 4,096 | ~250ms | OWASP recommended |
| 14 | 16,384 | ~1s | High-security systems |
| 16 | 65,536 | ~4s | Local key derivation |
Rule of thumb: Choose the highest cost that keeps login latency under 250ms on your production hardware.
2. Argon2id — The Modern Gold Standard
Argon2 won the 2015 Password Hashing Competition and comes in three variants:
| Variant | Protection Against | When to Use |
|---|---|---|
| Argon2d | GPU attacks (data-dependent) | NOT for passwords (side-channel vulnerable) |
| Argon2i | Side-channel attacks (data-independent) | Okay, but less GPU-resistant |
| Argon2id | Both GPU + side-channel attacks | ✅ Always use this for passwords |
Why Argon2id Is Superior
Unlike Bcrypt (which is CPU-bound), Argon2id is memory-hard. It forces the attacker to allocate large amounts of RAM per hash attempt, making GPU/ASIC-based cracking economically impractical.
OWASP-Recommended Parameters
Memory: 64 MB (65536 KiB)
Iterations: 3
Parallelism: 1 thread
Salt: 16 bytes (random)
Hash length: 32 bytes
Argon2id in Code
# Python (argon2-cffi)
from argon2 import PasswordHasher
ph = PasswordHasher(
memory_cost=65536, # 64 MB
time_cost=3, # 3 iterations
parallelism=1 # 1 thread
)
hash = ph.hash("CorrectHorseBatteryStaple")
# Returns: $argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
try:
ph.verify(hash, "CorrectHorseBatteryStaple")
print("Valid")
except Exception:
print("Invalid")
3. PBKDF2 — The Legacy Option
PBKDF2 (Password-Based Key Derivation Function 2) is the oldest of the three and is built into most platforms natively (including the Web Crypto API and .NET).
// Browser Web Crypto API
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]
);
const hash = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt: salt, iterations: 600000, hash: "SHA-256" },
keyMaterial, 256
);
OWASP recommends 600,000 iterations with SHA-256 for PBKDF2. However, PBKDF2 is not memory-hard, so it remains vulnerable to GPU acceleration.
Algorithm Decision Matrix
| Feature | Bcrypt | Argon2id | PBKDF2 |
|---|---|---|---|
| Memory-hard | ❌ | ✅ | ❌ |
| GPU-resistant | Moderate | ✅ Strong | ❌ Weak |
| Built-in salt | ✅ | ✅ | ❌ (manual) |
| Native browser support | ❌ | ❌ | ✅ (Web Crypto) |
| OWASP top pick (2024) | ✅ | ✅ (preferred) | ✅ (if others unavailable) |
| Max password length | 72 bytes | Unlimited | Unlimited |
Recommendation: Use Argon2id if your platform supports it. Fall back to Bcrypt (cost 12+) otherwise. Use PBKDF2 only when neither is available (e.g., browser-only environments).
Security Best Practices
- Always salt: Every hash must use a unique, cryptographically random salt (at least 16 bytes). Bcrypt and Argon2 handle this automatically.
- Never truncate passwords silently: Bcrypt has a 72-byte limit. If your app accepts longer passwords, hash with SHA-256 first (prehashing), then pass the result to Bcrypt.
- Rehash on login: If a user logs in and their stored hash uses outdated parameters (e.g., Bcrypt cost 8), verify the password and then re-hash with stronger parameters before storing.
- Use a pepper for defense-in-depth: A pepper is a server-side secret key applied before hashing. Store it in an HSM or environment variable — never in the database.
- Rate-limit login attempts: Even the strongest hash is useless if your login endpoint allows unlimited attempts.
Test your password hashing configuration with the Password Hash & Verifier tool. For related security topics, see the SSH Key Generation Guide and JWT Token Decoding.
Live Tool: Bcrypt / Argon2 / PBKDF2 Password Hash Verifier & Generator
Client-Side EngineGenerate Bcrypt, Argon2id, PBKDF2, and SCrypt hashes (with Bcrypt cost and PBKDF2 iteration controls) and verify passwords against Bcrypt hashes.