Introduction
Password hashing protects passwords even when databases get breached. A good hash is one-way: you can verify a password matches, but can’t reverse the hash to get the original password back.
Not all hashing algorithms are created equal. MD5 and SHA-256 are fast—too fast. Attackers can try billions of passwords per second with these. Modern password hashing algorithms are intentionally slow to make brute-force attacks impractical.
This guide compares bcrypt, Argon2, PBKDF2, and scrypt. You’ll learn which one to use, how quantum computing affects password security, and how to avoid costly mistakes.
How It Works
Password hashing uses three key concepts: hashing, salting, and key stretching.
Hashing converts passwords into fixed-length strings. Good hashes are:
- One-way: Can’t reverse the hash to get the password
- Deterministic: Same input always produces same output
- Avalanche effect: Tiny input change completely changes the hash
Salting adds random data before hashing. This prevents rainbow table attacks.
Without salt:
password123 → hash(password123) → 5f4dcc3b5aa765d61d8327deb882cf99
(Same hash for everyone using "password123")
With salt:
password123 + random_salt → hash(password123 + salt) → unique hash
(Different hash for each user, even with same password)
Key stretching runs the hash function thousands of times. This makes each attempt slower, forcing attackers to spend more time cracking passwords.
Fast hash (bad):
1 password = 0.000001 seconds
1 billion attempts = 16 minutes
Slow hash (good):
1 password = 0.1 seconds
1 billion attempts = 3 years
Why this matters: A GTX 4090 GPU can compute 200 billion MD5 hashes per second. With bcrypt, that drops to ~100,000 hashes per second. That’s 2 million times slower.
Best Practices
1. Use Argon2id for New Projects
Why: Argon2 won the Password Hashing Competition in 2015. It’s the most modern algorithm, resistant to GPU and ASIC attacks, and includes memory-hard features that make parallel cracking expensive.
How to implement:
# Python with argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # Number of iterations
memory_cost=65536, # 64 MB of memory
parallelism=4, # 4 parallel threads
hash_len=32, # 32-byte hash output
salt_len=16 # 16-byte salt
)
# Hash password
hash = ph.hash("user_password_here")
# → $argon2id$v=19$m=65536,t=2,p=4$...
# Verify password
try:
ph.verify(hash, "user_password_here")
# Password is correct
except:
# Password is wrong
// Node.js with argon2
const argon2 = require('argon2');
// Hash password
const hash = await argon2.hash('user_password_here', {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 2,
parallelism: 4
});
// Verify password
const isValid = await argon2.verify(hash, 'user_password_here');
When to use: All new applications. Argon2id is the default recommendation in 2025.
Argon2 variants:
- Argon2id: Hybrid (recommended for passwords)
- Argon2i: Optimized against side-channel attacks
- Argon2d: Optimized against GPU attacks (faster but less secure against side-channels)
2. Use bcrypt for Compatibility and Maturity
Why: bcrypt has been battle-tested for 25+ years. It’s widely supported, well-audited, and automatically salts passwords. Choose this if Argon2 isn’t available in your language/framework.
How to implement:
# Python with bcrypt
import bcrypt
# Hash password
password = b"user_password_here"
salt = bcrypt.gensalt(rounds=12) # Work factor: 2^12 iterations
hash = bcrypt.hashpw(password, salt)
# → $2b$12$...
# Verify password
if bcrypt.checkpw(password, hash):
print("Password correct")
// Node.js with bcrypt
const bcrypt = require('bcrypt');
// Hash password
const saltRounds = 12;
const hash = await bcrypt.hash('user_password_here', saltRounds);
// Verify password
const isValid = await bcrypt.compare('user_password_here', hash);
Work factor: Start with 12 rounds. Increase as hardware gets faster.
Rounds | Time per hash | Time for 1M passwords
-------|---------------|---------------------
10 | 0.1s | 27 hours
12 | 0.3s | 4.5 days
14 | 1.2s | 18 days
When to use: Legacy codebases, systems where Argon2 isn’t available, or when ecosystem support is critical.
3. Avoid PBKDF2 Unless Required by Standards
Why: PBKDF2 is older and less resistant to GPU attacks than bcrypt or Argon2. However, it’s required by many security standards (FIPS 140-2, NIST) and government systems.
How to implement:
# Python with hashlib
import hashlib
import os
salt = os.urandom(32)
hash = hashlib.pbkdf2_hmac(
'sha256', # Hash function
b'password', # Password bytes
salt, # Salt
100000, # Iterations (increase for stronger security)
dklen=32 # Output length
)
When to use: Only when compliance requires it (government, banking, healthcare). Otherwise, use Argon2 or bcrypt.
Iteration count: NIST recommends 210,000 iterations for PBKDF2-SHA256 (as of 2023).
4. Never Use MD5, SHA-1, or Plain SHA-256 for Passwords
Why: These algorithms are designed for speed, not password security. Attackers can compute billions of hashes per second on modern GPUs.
The problem:
# BAD: Fast hashing without salt or key stretching
import hashlib
hash = hashlib.sha256(b"password123").hexdigest()
# → 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
# An attacker can try 10 billion SHA-256 hashes per second
# This password would be cracked in < 1 second
Why it’s bad:
- No salt (rainbow tables work)
- Too fast (GPU can crack 10B+ hashes/sec)
- Not memory-hard (cheap to parallelize)
The fix: Use Argon2, bcrypt, or PBKDF2 with high iteration counts.
5. Store Hashes with Algorithm Metadata
Why: Algorithms and parameters need to be upgradeable. Store the algorithm version with the hash so you can migrate users to stronger hashing over time.
How to implement:
# Good: Hash includes algorithm info
argon2_hash = "$argon2id$v=19$m=65536,t=2,p=4$..."
bcrypt_hash = "$2b$12$..."
# Detect algorithm from hash format
if hash.startswith("$argon2"):
# Use Argon2 verification
elif hash.startswith("$2b$"):
# Use bcrypt verification
Migration strategy:
def verify_and_upgrade(user, password, hash):
# Verify with old algorithm
if verify_old_hash(password, hash):
# Rehash with new algorithm on successful login
new_hash = argon2.hash(password)
update_user_hash(user, new_hash)
return True
return False
Common Pitfalls
Using Fast Hashing Algorithms
The problem:
# Bad: Using SHA-256 directly
import hashlib
hash = hashlib.sha256(password.encode()).hexdigest()
Why it’s bad: SHA-256 runs at 10+ billion hashes/second on modern GPUs. An 8-character password gets cracked in hours.
The fix: Use Argon2, bcrypt, or PBKDF2.
# Good: Using proper password hashing
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash(password)
Not Salting Passwords
The problem:
# Bad: Same password = same hash
hash1 = hash_password("password123") # → abc123...
hash2 = hash_password("password123") # → abc123... (identical!)
Why it’s bad: Attackers build rainbow tables of pre-computed hashes. If 1,000 users have “password123”, all 1,000 get cracked at once.
The fix: Modern algorithms (Argon2, bcrypt) handle salting automatically. If you must use PBKDF2, generate a unique salt per password.
# Good: Each hash gets a unique salt
import os
salt1 = os.urandom(32)
salt2 = os.urandom(32)
hash1 = pbkdf2(password, salt1) # Different hash
hash2 = pbkdf2(password, salt2) # Different hash
Using Weak Parameters
The problem:
# Bad: Too few iterations
bcrypt.gensalt(rounds=4) # Only 2^4 = 16 iterations (cracks in seconds)
Why it’s bad: Low work factors make hashing fast, defeating the purpose of key stretching.
The fix: Use recommended parameters for each algorithm.
# Good: Strong parameters
bcrypt.gensalt(rounds=12) # 2^12 = 4,096 iterations
argon2.hash(password,
time_cost=2,
memory_cost=65536, # 64 MB
parallelism=4
)
Algorithm Comparison Table
| Algorithm | Year | Speed | GPU Resistance | Memory Hard | Quantum Safe | Recommendation |
|---|---|---|---|---|---|---|
| Argon2id | 2015 | Slow | ✅ Excellent | ✅ Yes | ⚠️ Partial | ✅ Use for new projects |
| bcrypt | 1999 | Slow | ✅ Good | ❌ No | ⚠️ Partial | ✅ Use if Argon2 unavailable |
| scrypt | 2009 | Slow | ✅ Good | ✅ Yes | ⚠️ Partial | ⚠️ Use Argon2 instead |
| PBKDF2 | 2000 | Medium | ⚠️ Fair | ❌ No | ⚠️ Partial | ⚠️ Only for compliance |
| SHA-256 | 2001 | Fast | ❌ Poor | ❌ No | ⚠️ Partial | ❌ Never for passwords |
| MD5 | 1992 | Fast | ❌ Poor | ❌ No | ❌ No | ❌ Broken, never use |
Notes:
- GPU Resistance: How well the algorithm resists GPU-based cracking
- Memory Hard: Requires significant RAM, making ASIC attacks expensive
- Quantum Safe: No current password hash is fully quantum-resistant. Strong passwords (20+ chars) remain secure.
Quantum Computing Considerations
Current status: Quantum computers threaten encryption (RSA, ECC) but not password hashing directly.
Why password hashes are relatively safe:
- Hash functions (SHA-2, SHA-3) have no known efficient quantum attacks
- Grover’s algorithm provides ~2x speedup (not exponential like Shor’s algorithm)
- A 256-bit hash requires 2^128 quantum operations (still impractical)
What this means:
- Short passwords remain vulnerable: Quantum or not, “password123” cracks instantly
- Long passwords stay secure: 20+ character passwords are safe against quantum attacks
- Hash algorithms don’t need changing: Argon2, bcrypt, and SHA-256 remain quantum-resistant
Future-proofing:
- Enforce minimum 16-character passwords (better: 20+)
- Use Argon2id with high memory costs
- Monitor post-quantum cryptography standards (NIST PQC)
Bottom line: Focus on password length, not quantum-resistant hashing. A 20-character password is more important than the hashing algorithm.
Quick Reference Checklist
Choosing a password hashing algorithm:
- Use Argon2id (recommended for 2025)
- Use bcrypt if Argon2 isn’t available
- Use PBKDF2 only for compliance requirements
- Never use MD5, SHA-1, or plain SHA-256
Implementation checklist:
- Hash passwords server-side (never client-side only)
- Use unique salt per password (automatic in Argon2/bcrypt)
- Store algorithm metadata with hash (for future upgrades)
- Use strong parameters (bcrypt rounds ≥ 12, Argon2 memory ≥ 64MB)
- Increase work factors as hardware improves
Security checklist:
- Enforce minimum 12-character passwords (16+ recommended)
- Rehash passwords with stronger algorithms on user login
- Use timing-safe comparison for hash verification
- Log failed authentication attempts
- Implement account lockout after repeated failures
Standards and References
- OWASP Password Storage Cheat Sheet: Best practices for password hashing
- NIST SP 800-63B: Digital identity guidelines (recommends PBKDF2 with 210k+ iterations)
- Password Hashing Competition: Argon2 winner (2015)
- RFC 2898: PBKDF2 specification
- RFC 9106: Argon2 specification (draft)
Summary
Use Argon2id for new projects. It’s the most secure option against modern attacks. Use bcrypt if Argon2 isn’t available—it’s battle-tested and widely supported. Avoid PBKDF2 unless compliance requires it.
Never use MD5 or plain SHA-256 for passwords. They’re designed for speed, not security. Attackers can compute billions of hashes per second on GPUs.
Quantum computing doesn’t break password hashing. Focus on password length (16+ characters) and proper algorithm selection (Argon2 or bcrypt).
Key takeaways:
- Argon2id is best: Memory-hard, GPU-resistant, modern standard
- bcrypt is solid: 25+ years of battle testing, excellent fallback
- Never use MD5/SHA: Too fast, attackers can crack billions per second
- Quantum isn’t the threat: Short passwords are. Enforce 16+ characters.
- Upgrade over time: Rehash passwords with stronger algorithms on user login