Choosing the right password hashing algorithm is one of the most critical security decisions you will make in any application that handles user credentials. A weak choice can expose millions of passwords in a data breach, while the right algorithm can make brute-force attacks economically infeasible. This comprehensive guide compares the three leading password hashing algorithms โ Bcrypt, Scrypt, and Argon2 โ so you can make an informed decision for your project.
Why Password Hashing Matters
Password hashing is a one-way transformation that converts a plaintext password into a fixed-length string of characters. Unlike encryption, hashing is irreversible by design โ you cannot recover the original password from its hash. When a user logs in, the system hashes the submitted password and compares it to the stored hash.
Storing passwords in plaintext or using fast general-purpose hash functions like MD5 or SHA-256 is dangerously insecure. Modern GPUs can compute billions of SHA-256 hashes per second, meaning an attacker with a stolen database can crack most passwords in hours or days.
Purpose-built password hashing algorithms solve this by being intentionally slow and resource-intensive. They incorporate three key defenses:
- Salt: Salting: A random value added to each password before hashing, ensuring identical passwords produce different hashes.
- Cost: Cost factor: A configurable work parameter that controls how slow the hash computation is, allowing you to increase difficulty as hardware improves.
- Memory: Memory hardness: Some algorithms require large amounts of RAM, making attacks with specialized hardware (GPUs, ASICs, FPGAs) far more expensive.
# Comparison: General-purpose vs Password hashing speed
SHA-256: ~10,000,000,000 hashes/sec (GPU) โ Dangerously fast
MD5: ~25,000,000,000 hashes/sec (GPU) โ Even worse
Bcrypt: ~ 50,000 hashes/sec (GPU) โ 200,000x slower
Argon2id: ~ 1,000 hashes/sec (GPU) โ 10,000,000x slower (with 19MB memory)Bcrypt: The Battle-Tested Standard
How Bcrypt Works
Bcrypt was designed in 1999 by Niels Provos and David Mazieres, based on the Blowfish cipher. It uses a cost factor (work factor) that determines the number of iterations: the hash computation runs 2^cost rounds of the expensive key setup phase. The default cost factor is 10 (1,024 rounds), but modern recommendations suggest 12-14 (4,096-16,384 rounds).
A bcrypt hash string contains everything needed for verification:
$2b$12$LJ3m4ys3Lg2VBe8EWdKFTe8x7N3fWs0YhW1qDZGhIJFG3rJ1/Bxiy
โโ โโ โโโโโโโโโโโโโโโโโโโโโโโ Hash (31 chars, Base64)
โโ โโโโโโโโโโโโโโโโโโโโโโโโโโ Salt (22 chars, Base64, 128-bit)
โโ โโโโโโโโโโโโโโโโโโโโโโโโโโ Cost factor (12 = 2^12 = 4,096 rounds)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Version (2b = current)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Algorithm identifier ($2b$ = bcrypt)Bcrypt Pros
- Extremely well-tested: 25+ years of cryptographic scrutiny with no practical attacks found.
- Built-in salt: Automatically generates and stores a 128-bit random salt with each hash.
- Adaptive cost: The work factor can be increased over time as hardware gets faster.
- Universal library support: Available in virtually every programming language and framework.
- Simple API: Easy to use correctly, reducing the chance of implementation mistakes.
Bcrypt Cons
- CPU-only hardness: Bcrypt is not memory-hard, making it vulnerable to GPU and ASIC-based attacks (though still expensive).
- 72-byte password limit: Bcrypt truncates passwords longer than 72 bytes. This rarely matters in practice but is a design limitation.
- Fixed memory usage: Uses only 4KB of RAM regardless of cost factor, which does not increase resistance to parallel attacks.
- No parallelism parameter: Cannot leverage multi-core CPUs for legitimate hashing while remaining single-threaded for attackers.
When to Use Bcrypt
Bcrypt is an excellent default choice for most applications. Use it when you need a proven, widely-supported algorithm and your threat model does not require memory hardness. It is the safest "I do not want to think about this too much" choice.
Scrypt: Memory-Hard Pioneer
How Scrypt Works
Scrypt was designed in 2009 by Colin Percival for the Tarsnap online backup service. Its key innovation is memory hardness: the algorithm requires a large amount of RAM proportional to its difficulty setting. This makes parallel attacks on GPUs, ASICs, and FPGAs significantly more expensive because each parallel instance needs its own block of memory.
Scrypt has three configurable parameters:
- N: N (CPU/memory cost): Determines the memory and CPU cost. Must be a power of 2. Higher N means more memory and time. Common values: 2^14 (16,384) to 2^20 (1,048,576).
- r: r (block size): Controls the sequential memory read size. Typical value: 8. Increasing r increases memory usage linearly.
- p: p (parallelism): The number of parallel chains. Typical value: 1. Increasing p allows parallelizing computation on the defender side.
# Scrypt memory calculation
Memory = 128 * N * r bytes
N = 2^14 (16384), r = 8:
128 * 16384 * 8 = 16,777,216 bytes = 16 MB per hash
N = 2^15 (32768), r = 8:
128 * 32768 * 8 = 33,554,432 bytes = 32 MB per hash
N = 2^20 (1048576), r = 8:
128 * 1048576 * 8 = 1,073,741,824 bytes = 1 GB per hashMemory usage formula: 128 * N * r bytes. With N=2^14 and r=8, that is 128 * 16384 * 8 = 16 MB per hash.
Scrypt Pros
- Memory hardness: Requires significant RAM per hash, making GPU/ASIC attacks much more expensive.
- Configurable parameters: Separate control over CPU cost, memory cost, and parallelism.
- Proven in production: Used by Litecoin, Dogecoin, and many cryptocurrency systems since 2011.
- Good security margin: No practical attacks found in 15+ years.
Scrypt Cons
- Complex parameter tuning: Three interdependent parameters (N, r, p) make it harder to configure correctly than bcrypt.
- Time-memory tradeoff vulnerability: An attacker can reduce memory requirements by spending more CPU time, partially defeating the memory-hardness guarantee.
- Fewer built-in libraries: Not as universally supported as bcrypt, especially in older frameworks.
- No side-channel resistance: Not specifically designed to resist cache-timing attacks.
When to Use Scrypt
Use scrypt when you need memory hardness and Argon2 is not available in your environment. It is a solid choice for applications where you want to make GPU-based cracking significantly more expensive.
Argon2 (Argon2id): The Modern Gold Standard
How Argon2 Works
Argon2 won the Password Hashing Competition (PHC) in 2015, a multi-year open competition to find the best password hashing algorithm. It was designed by Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich. Argon2 comes in three variants:
Argon2 has three main parameters:
- m: m (memory cost): The amount of memory in KiB. OWASP recommends at least 19 MiB (19456 KiB) for Argon2id. Higher is better.
- t: t (time cost / iterations): The number of passes over memory. Minimum 2 for Argon2id. More iterations = slower hashing.
- p: p (parallelism): The number of threads. Set to the number of CPU cores available for hashing. Typical value: 1-4.
# Argon2id hash format
$argon2id$v=19$m=19456,t=2,p=1$c2FsdHNhbHRzYWx0$W1BLEn4OWxCjU2F...
โโ โโ โโ โโ โโ โโโ Hash (Base64)
โโ โโ โโ โโ โโโโ Salt (Base64)
โโ โโ โโ โโ โโโโโ p=1 (parallelism)
โโ โโ โโ โโโโโโโโโโโโ t=2 (iterations)
โโ โโ โโ โโโโโโโโโโโโ m=19456 (memory in KiB = 19 MiB)
โโ โโ โโโโโโโโโโโโโโโโโโโโโ v=19 (version 1.3)
โโ โโโโโโโโโโโโโโโโโโโโโโโโโ argon2id (variant)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Algorithm identifierArgon2 Pros
- PHC winner: Rigorously evaluated by the global cryptographic community in a multi-year competition.
- Superior memory hardness: No known time-memory tradeoff attacks (unlike scrypt).
- Side-channel resistance: Argon2id hybrid mode provides both GPU resistance and side-channel resistance.
- Three independent parameters: Fine-grained control over memory, time, and parallelism.
- Modern design: Incorporates lessons learned from bcrypt and scrypt shortcomings.
- OWASP recommended: The primary recommendation in the OWASP Password Storage Cheat Sheet.
Argon2 Cons
- Younger algorithm: Only since 2015, so it has fewer years of real-world deployment than bcrypt (1999).
- Library availability: Not as universally supported as bcrypt in all languages and frameworks, though adoption is growing rapidly.
- Parameter complexity: Like scrypt, tuning three parameters requires some understanding of your deployment environment.
- Memory allocation overhead: Allocating and freeing large blocks of memory per hash can impact performance in high-throughput scenarios.
When to Use Argon2
Argon2id is the recommended choice for new applications if your language or framework has a mature library. It provides the best overall security properties. If you are building anything that stores passwords today, Argon2id should be your first choice.
Side-by-Side Comparison
| Feature | Bcrypt | Scrypt | Argon2id |
|---|---|---|---|
| Year Introduced | 1999 | 2009 | 2015 |
| Memory Hardness | None (4KB fixed) | Yes (configurable) | Yes (configurable) |
| Side-Channel Resistance | N/A | No | Yes (hybrid mode) |
| GPU/ASIC Resistance | Moderate | High | Highest |
| Configurable Parameters | 1 (cost factor) | 3 (N, r, p) | 3 (m, t, p) |
| Max Password Length | 72 bytes | Unlimited | Unlimited |
| Library Adoption | Universal | Good | Growing rapidly |
| OWASP Recommendation | Acceptable (2nd choice) | Acceptable (3rd choice) | Primary recommendation |
| Configuration Complexity | Very Low | Medium | Medium |
| Hashing Speed (default) | ~300ms (cost=12) | ~100ms (N=2^14) | ~200ms (19MiB, t=2) |
Code Examples
Node.js Examples
// ============================================
// Bcrypt (Node.js) - using 'bcrypt' package
// ============================================
const bcrypt = require('bcrypt');
// Hash a password
const saltRounds = 12; // Cost factor: 2^12 = 4,096 iterations
const password = 'mySecurePassword123';
// Async (recommended)
const hash = await bcrypt.hash(password, saltRounds);
// => "$2b$12$LJ3m4ys3Lg2VBe8EWdKFTe..."
// Verify a password
const isMatch = await bcrypt.compare(password, hash);
// => true
// ============================================
// Scrypt (Node.js) - built-in crypto module
// ============================================
const crypto = require('crypto');
// Hash a password
const salt = crypto.randomBytes(16);
const N = 32768; // CPU/memory cost (2^15)
const r = 8; // Block size
const p = 1; // Parallelism
const keyLen = 64; // Output length in bytes
const scryptHash = crypto.scryptSync(password, salt, keyLen, { N, r, p });
// Store both salt and hash (e.g., as hex strings)
const stored = salt.toString('hex') + ':' + scryptHash.toString('hex');
// Verify: split stored value, re-derive, and compare
const [storedSalt, storedHash] = stored.split(':');
const derivedHash = crypto.scryptSync(
password,
Buffer.from(storedSalt, 'hex'),
keyLen,
{ N, r, p }
);
const isValid = crypto.timingSafeEqual(
Buffer.from(storedHash, 'hex'),
derivedHash
);
// ============================================
// Argon2 (Node.js) - using 'argon2' package
// ============================================
const argon2 = require('argon2');
// Hash a password (Argon2id is the default)
const argon2Hash = await argon2.hash(password, {
type: argon2.argon2id, // Use Argon2id variant
memoryCost: 19456, // 19 MiB (OWASP minimum)
timeCost: 2, // 2 iterations
parallelism: 1, // 1 thread
});
// => "$argon2id$v=19$m=19456,t=2,p=1$..."
// Verify a password
const isArgon2Match = await argon2.verify(argon2Hash, password);
// => true
// Check if hash needs rehashing (e.g., after config change)
const needsRehash = argon2.needsRehash(argon2Hash, {
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});Python Examples
# ============================================
# Bcrypt (Python) - using 'bcrypt' package
# ============================================
import bcrypt
password = b"mySecurePassword123"
# Hash a password
salt = bcrypt.gensalt(rounds=12) # Cost factor 12
hashed = bcrypt.hashpw(password, salt)
# => b"$2b$12$LJ3m4ys3Lg2VBe8EWdKFTe..."
# Verify a password
is_valid = bcrypt.checkpw(password, hashed)
# => True
# ============================================
# Scrypt (Python) - using hashlib (built-in)
# ============================================
import hashlib
import os
import hmac
password = b"mySecurePassword123"
salt = os.urandom(16)
# Hash a password
scrypt_hash = hashlib.scrypt(
password,
salt=salt,
n=32768, # CPU/memory cost (2^15)
r=8, # Block size
p=1, # Parallelism
dklen=64 # Output length
)
# Store salt + hash together
stored = salt.hex() + ":" + scrypt_hash.hex()
# Verify: re-derive and use constant-time comparison
stored_salt, stored_hash = stored.split(":")
derived = hashlib.scrypt(
password,
salt=bytes.fromhex(stored_salt),
n=32768, r=8, p=1, dklen=64
)
is_valid = hmac.compare_digest(
bytes.fromhex(stored_hash), derived
)
# ============================================
# Argon2 (Python) - using 'argon2-cffi' package
# ============================================
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # 2 iterations
memory_cost=19456, # 19 MiB
parallelism=1, # 1 thread
hash_len=32, # Output hash length
salt_len=16, # Salt length
)
# Hash a password
argon2_hash = ph.hash("mySecurePassword123")
# => "$argon2id$v=19$m=19456,t=2,p=1$..."
# Verify a password
try:
ph.verify(argon2_hash, "mySecurePassword123")
print("Password is correct")
except Exception:
print("Password is incorrect")
# Check if hash needs rehashing
if ph.check_needs_rehash(argon2_hash):
# Re-hash with current parameters
new_hash = ph.hash("mySecurePassword123")
# Update stored hash in databaseRecommendation Decision Guide
Use this decision guide to choose the right algorithm for your project:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Password Hashing Algorithm Decision Guide โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Is Argon2id available with โ
โ a mature library in your โ
โ language / framework? โ
โโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ โ
YES NO
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Use Argon2id โ โ Do you need memory โ
โ m=19456 (19MB) โ โ hardness for GPU/ASIC โ
โ t=2, p=1 โ โ resistance? โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโ
โ โ
YES NO
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Use Scrypt โ โ Use Bcrypt โ
โ N=2^15, r=8 โ โ cost factor=12+ โ
โ p=1 โ โ โ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโYES: Use Argon2id with OWASP-recommended settings (m=19456, t=2, p=1).
NO: Continue to next question.
YES: Use Scrypt with N=2^15, r=8, p=1 (or higher N if your server can handle it).
NO: Use Bcrypt with a cost factor of 12 or higher.
Do not force all users to reset passwords. Instead, wrap the old hash: Argon2id(existing_sha256_hash). On next login, re-hash with Argon2id directly.
Summary: For new projects in 2024+, use Argon2id. For existing projects using bcrypt, there is no urgent need to migrate โ bcrypt is still secure. If you are choosing between bcrypt and scrypt, bcrypt is simpler and equally safe for most threat models.
OWASP Recommended Settings
The OWASP Password Storage Cheat Sheet provides these minimum recommended configurations:
| Algorithm | Recommended Configuration | Notes |
|---|---|---|
| Argon2id | m=19456 (19 MiB), t=2, p=1 | Primary recommendation. Increase m if server RAM allows. |
| Bcrypt | cost=12 (minimum 10) | Second choice. Increase cost factor to target 200-500ms. |
| Scrypt | N=2^17 (131072), r=8, p=1 | Third choice. Memory = 128 * N * r = 128 MB per hash. |
Migrating Between Algorithms
If you need to migrate from one hashing algorithm to another without forcing password resets, use the "wrap and upgrade" strategy:
// Migration example: bcrypt โ Argon2id (Node.js)
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
async function verifyAndUpgrade(password, storedHash, userId) {
// Check hash version
if (storedHash.startsWith('$argon2id$')) {
// Already using Argon2id โ verify directly
return await argon2.verify(storedHash, password);
}
if (storedHash.startsWith('$2b$')) {
// Still using bcrypt โ verify with bcrypt
const isValid = await bcrypt.compare(password, storedHash);
if (isValid) {
// Upgrade: re-hash directly with Argon2id
const newHash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});
// Update database
await db.query(
'UPDATE users SET password_hash = $1, hash_version = $2 WHERE id = $3',
[newHash, 'argon2id', userId]
);
}
return isValid;
}
throw new Error('Unknown hash format');
}Important: Important: Never store plaintext passwords, even temporarily during migration. The wrapping approach ensures all hashes are always protected by at least one strong algorithm.
Common Implementation Pitfalls
A global salt (same salt for all passwords) defeats the purpose. If one password is cracked, all identical passwords are immediately exposed. All three algorithms (bcrypt, scrypt, Argon2) generate unique random salts per hash by default โ do not override this behavior.
Using the minimum cost factor makes hashing too fast. Aim for 200-500ms per hash on your production hardware. Benchmark on your actual server and increase the cost factor until you reach the target latency.
A pepper (server-side secret added to passwords before hashing) can add defense-in-depth, but only if stored securely outside the database (e.g., in a hardware security module or environment variable). If the pepper is compromised alongside the database, it provides no benefit.
As hardware improves, you should periodically increase the cost factor. When a user logs in successfully, check if their hash uses an outdated cost factor and re-hash with the current settings.
Always use constant-time comparison functions when verifying hashes. All reputable password hashing libraries include this, but if you are comparing hashes manually, use a dedicated constant-time comparison function.
Performance Benchmarks
These benchmarks were measured on a standard 4-core server CPU (Intel Xeon E-2236, 3.4GHz) with 16GB RAM. Actual performance will vary by hardware.
| Algorithm & Config | Time per Hash | Memory per Hash | Attacker Rate (RTX 4090) |
|---|---|---|---|
| Bcrypt (cost=10) | ~75ms | 4 KB | ~150K hashes/sec |
| Bcrypt (cost=12) | ~300ms | 4 KB | ~38K hashes/sec |
| Bcrypt (cost=14) | ~1.2s | 4 KB | ~9.5K hashes/sec |
| Scrypt (N=2^14, r=8) | ~100ms | 16 MB | ~5K hashes/sec |
| Scrypt (N=2^15, r=8) | ~200ms | 32 MB | ~2.5K hashes/sec |
| Scrypt (N=2^17, r=8) | ~800ms | 128 MB | ~300 hashes/sec |
| Argon2id (19MiB, t=2) | ~200ms | 19 MB | ~1K hashes/sec |
| Argon2id (64MiB, t=3) | ~800ms | 64 MB | ~150 hashes/sec |
| Argon2id (256MiB, t=4) | ~3.5s | 256 MB | ~20 hashes/sec |
Frequently Asked Questions
Is bcrypt still safe to use in 2024?
Yes, bcrypt is still considered safe with a cost factor of 12 or higher. No practical attacks have been found against bcrypt in over 25 years. However, for new projects, Argon2id is the preferred choice because it provides additional defense against GPU-based attacks through memory hardness.
What happens if I use SHA-256 for password hashing?
SHA-256 is a general-purpose hash function designed for speed, not for password hashing. A modern GPU can compute over 10 billion SHA-256 hashes per second. This means an attacker can brute-force most passwords in hours. Always use a purpose-built password hashing algorithm (bcrypt, scrypt, or Argon2).
How do I choose the right cost factor / work parameters?
The goal is to make each hash take 200-500 milliseconds on your production server. Start with the recommended defaults and benchmark on your actual hardware. Increase parameters until you reach the target latency. Remember that login endpoints typically do not need to handle thousands of requests per second, so you can afford more expensive hashing.
Can I use Argon2 for anything other than passwords?
Yes, Argon2 can be used for key derivation (deriving encryption keys from passwords), proof-of-work systems, and cryptocurrency mining. The Argon2d variant is specifically designed for use cases where side-channel attacks are not a concern, such as cryptocurrency mining.
Should I add a pepper in addition to a salt?
A pepper (a secret key stored separately from the database) can provide defense-in-depth. If an attacker obtains only the database but not the application server or key management system, the pepper prevents offline cracking. However, it adds complexity and must be managed carefully (key rotation, secure storage). For most applications, a well-configured Argon2id hash with a strong cost factor is sufficient.
How often should I increase the cost factor?
Review your password hashing parameters annually or whenever you upgrade server hardware. Moore's Law suggests doubling CPU power roughly every 18 months, which means you should increase your cost factor by 1 (doubling the work) approximately every 18-24 months. Implement "hash upgrading" โ re-hash passwords with the new cost factor when users log in successfully.