Password Security in 2025: Which Hashing Algorithms Are Actually Safe?
TL;DR (2025)
- Best algorithm: Argon2id
- Still safe: Bcrypt (cost ≥ 12)
- Avoid: SHA-256, SHA-512, MD5
- PBKDF2: Good for compliance/NIST
- Goal: 200–500ms per hash
- Always: Salts, rate limiting, timing-safe compare
You're building a new app, and you need to store user passwords. Simple, right? Just hash them and you're done.
Wrong.
I've seen too many developers make the same mistakes: using SHA-256 for passwords (it's fast, but that's the problem), forgetting salts, or using outdated algorithms. In 2025, password security isn't just about hashing—it's about choosing the right algorithm and implementing it correctly.
This guide cuts through the noise. We'll compare the major algorithms, show you what actually works, and give you real code examples you can use today.
Test the algorithms yourself: You can directly test how fast different hashes are with our Bcrypt Generator and Hash Generator tools.
Why Password Hashing Matters (The Real Story)
Let me paint you a picture. Your database gets breached (it happens more than you think). If you stored passwords in plaintext, attackers now have every user's password. Game over.
Hashing converts passwords into gibberish strings that can't be reversed. Even if attackers steal your database, they can't read the passwords directly. But here's the catch: not all hashing is equal.
The problem with fast hashes: Algorithms like SHA-256 or MD5 were designed for data integrity, not security. They're fast—too fast. Attackers can compute billions of hashes per second on a single GPU. Your "secure" password? Cracked in minutes.
The solution: Use slow, purpose-built password hashing algorithms. We're talking Bcrypt, Argon2, or PBKDF2. These algorithms are intentionally slow, making brute-force attacks impractical.
The Three Pillars: Salting, Peppering, and Key Stretching
Before we dive into specific algorithms, let's cover the fundamentals that make password hashing secure.
Salting: Your First Line of Defense
A salt is random data added to each password before hashing. Why? Because identical passwords should produce different hashes. Without salts, attackers can use rainbow tables—precomputed hash databases—to crack passwords instantly.
Here's how it works in practice:
// Node.js: Bcrypt handles salting automatically
const bcrypt = require('bcrypt');
async function hashPassword(password) {
// Bcrypt generates a unique salt for each password
// Cost factor 12 = good balance of security and speed
const hash = await bcrypt.hash(password, 12);
return hash; // Format: $2b$12$salt+hash (salt is embedded)
}
// Verify password
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
## Python: Bcrypt with automatic salting
import bcrypt
def hash_password(password: str) -> str:
# Generate salt and hash in one step
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed.decode('utf-8')
def verify_password(password: str, hash: str) -> bool:
return bcrypt.checkpw(password.encode('utf-8'), hash.encode('utf-8'))
Key point: Modern algorithms (Bcrypt, Argon2, PBKDF2) handle salting automatically. You don't need to manage salts manually.
Peppering: Extra Security Layer
A pepper is a secret value stored separately from your database (usually in environment variables). Unlike salts (unique per password), peppers are the same for all passwords but kept secret.
Think of it as defense in depth: even if attackers compromise your database, they still need the pepper to crack passwords.
// Node.js: Adding a pepper
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const PEPPER = process.env.PASSWORD_PEPPER; // Store in .env, never in code
async function hashPasswordWithPepper(password) {
// Add pepper before hashing
const pepperedPassword = password + PEPPER;
// Bcrypt adds its own salt on top
const hash = await bcrypt.hash(pepperedPassword, 12);
return hash;
}
When to use peppering: It's optional but recommended for high-security applications. The trade-off is complexity—if you lose the pepper, you can't verify passwords.
Key Stretching: Making Attacks Expensive
Key stretching makes hashing computationally expensive by iterating the hash function thousands or millions of times. Each password verification takes time—maybe 200-500ms. That's slow enough to make brute-force attacks impractical.
Modern password hashing algorithms incorporate key stretching by design. Bcrypt uses a "cost factor" that doubles computation time with each increment. Argon2 lets you tune memory, time, and parallelism costs.
The Algorithms: What Actually Works in 2025
Let's cut to the chase. Here are the algorithms you should care about, ranked by security and practicality.
Bcrypt: The Battle-Tested Workhorse
Bcrypt has been around since 1999. It's the algorithm most developers know, and for good reason: it works. You can test Bcrypt hashing with our Bcrypt Generator tool to see how different cost factors affect performance.
How it works: Bcrypt uses a cost factor (work factor) that determines iterations. Cost factor 12 = 2^12 = 4,096 iterations. Each increment doubles computation time.
// Node.js: Bcrypt implementation
const bcrypt = require('bcrypt');
async function secureHash(password) {
// Cost factor 12 = good for 2025
// Adjust based on your server's performance
const hash = await bcrypt.hash(password, 12);
return hash;
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
# Python: Bcrypt
import bcrypt
password = "mySecurePassword123"
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
# Verify
is_valid = bcrypt.checkpw(password.encode('utf-8'), hashed)
Bcrypt strengths:
- Proven track record (20+ years)
- Widely supported (every language has a library)
- Adaptive (increase cost factor as hardware improves)
- Built-in salting (automatic)
Bcrypt weaknesses:
- GPU vulnerable (can be accelerated, though still slower than SHA-256)
- No memory hardness (ASIC attacks are feasible)
- Based on Blowfish (may become vulnerable over time)
Is Bcrypt still safe in 2025? Yes, with cost factor 12 or higher. For new projects, consider Argon2 for better hardware resistance. For existing systems, Bcrypt is fine—just make sure your cost factor is high enough.
Argon2: The Modern Champion
Argon2 won the Password Hashing Competition in 2015. It's what security experts recommend for new projects. Why? Memory hardness.
Argon2 requires significant RAM to compute efficiently. GPUs and ASICs have limited memory compared to CPUs, making attacks much more expensive.
// Node.js: Argon2 implementation
const argon2 = require('argon2');
async function hashWithArgon2(password) {
const hash = await argon2.hash(password, {
type: argon2.argon2id, // Use hybrid variant (recommended)
memoryCost: 65536, // 64 MB memory
timeCost: 3, // 3 iterations
parallelism: 4, // 4 threads
});
return hash;
}
async function verifyArgon2(password, hash) {
return await argon2.verify(hash, password);
}
# Python: Argon2
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # Memory in KB (64 MB)
parallelism=4, # Number of parallel threads
hash_len=32, # Hash length
salt_len=16 # Salt length
)
# Hash password
password = "mySecurePassword123"
hash = ph.hash(password)
# Verify password
try:
ph.verify(hash, password)
print("Password is valid")
except:
print("Password is invalid")
Argon2 strengths:
- Memory hardness (resists GPU/ASIC attacks)
- Configurable (tune memory, time, parallelism)
- Modern design (built for current threats)
- IETF standard (RFC 9106)
Argon2 weaknesses:
- Newer (less battle-tested than Bcrypt)
- More complex (requires parameter tuning)
- Resource intensive (may be too slow for high-traffic apps)
Best for: New applications, high-security requirements, systems with abundant memory.
PBKDF2: The NIST Standard
PBKDF2 is NIST-approved and widely adopted in enterprise environments. It's flexible (you choose the underlying hash function) but requires careful implementation.
// Node.js: PBKDF2 implementation
const crypto = require('crypto');
function hashWithPBKDF2(password, salt) {
// Use SHA-256 as the underlying hash function
// 100,000 iterations minimum (NIST recommendation)
const iterations = 100000;
const keyLength = 64; // 64 bytes = 512 bits
const hash = crypto.pbkdf2Sync(
password,
salt,
iterations,
keyLength,
'sha256'
);
// Store: iterations:salt:hash (all base64 encoded)
return {
iterations,
salt: salt.toString('base64'),
hash: hash.toString('base64')
};
}
function verifyPBKDF2(password, storedSalt, storedHash, iterations) {
const salt = Buffer.from(storedSalt, 'base64');
const hash = Buffer.from(storedHash, 'base64');
const computedHash = crypto.pbkdf2Sync(
password,
salt,
iterations,
hash.length,
'sha256'
);
// Use timing-safe comparison
return crypto.timingSafeEqual(hash, computedHash);
}
PBKDF2 strengths:
- NIST approved (good for compliance)
- Flexible (choose hash function)
- Well-documented
PBKDF2 weaknesses:
- No memory hardness (GPU/ASIC vulnerable)
- Manual implementation (must handle salts yourself)
- Less efficient (slower than Bcrypt for equivalent security)
Best for: Enterprise applications, compliance requirements, systems needing NIST standards.
SHA-256 and SHA-512: Why They're Wrong for Passwords
SHA-256 and SHA-512 are cryptographic hash functions, but they're not designed for password hashing. They're fast—too fast for security.
// Node.js: SHA-256 (NOT RECOMMENDED for passwords)
const crypto = require('crypto');
function hashWithSHA256(password) {
// This is FAST - attackers can compute billions per second
return crypto.createHash('sha256').update(password).digest('hex');
}
// An attacker can try millions of passwords per second
// This is why SHA-256 is insecure for password storage
Is SHA-256 secure? For data integrity, yes. For password hashing, absolutely not. SHA-256 can compute over 1 billion hashes per second on modern hardware. Your password? Cracked in minutes.
When SHA-256 is acceptable: When used with PBKDF2 as the underlying hash function, or for data integrity checks. Never use it directly for passwords.
scrypt: The Memory-Hard Alternative
scrypt is memory-hard like Argon2 but older. It's used by many cryptocurrency systems.
// Node.js: scrypt implementation
const crypto = require('crypto');
function hashWithScrypt(password, salt) {
const keyLength = 64;
const cost = 16384; // CPU/memory cost parameter (2^14)
const blockSize = 8; // Block size parameter
const parallelization = 1; // Parallelization parameter
const hash = crypto.scryptSync(
password,
salt,
keyLength,
{
cost,
blockSize,
parallelization
}
);
return {
cost,
blockSize,
parallelization,
salt: salt.toString('base64'),
hash: hash.toString('base64')
};
}
Best for: Systems already using scrypt, cryptocurrency applications.
Algorithm Comparison: The 2025 Reality Check
| Algorithm | Speed | GPU Resistance | ASIC Resistance | Memory Hardness | Recommended? |
|---|---|---|---|---|---|
| Argon2id | Medium | Excellent | Excellent | Yes | ✅ Best Choice |
| Bcrypt | Medium | Good | Moderate | No | ✅ Still Safe |
| PBKDF2 | Medium | Moderate | Poor | No | ✅ Acceptable |
| scrypt | Medium | Good | Good | Yes | ✅ Good |
| SHA-256 | Very Fast | Poor | Poor | No | Never |
| SHA-512 | Fast | Poor | Poor | No | Never |
| MD5 | Very Fast | Poor | Poor | No | Deprecated |
Want to test these algorithms? Use our Hash Generator to compare SHA-256, SHA-512, and MD5 (for data integrity, not passwords!), or our Bcrypt Generator to test Bcrypt hashing with different cost factors.
Performance Benchmarks (2025 Hardware)
- SHA-256: ~1 billion hashes/second
- SHA-512: ~500 million hashes/second
- PBKDF2-SHA256 (100k iterations): ~1,000 hashes/second
- Bcrypt (cost 12): ~100 hashes/second
- Argon2id (t=3, m=64MB): ~50 hashes/second
- scrypt (N=16384): ~30 hashes/second
Best Practices: What Actually Works
Here's what I've learned from building secure systems and seeing what breaks in production.
1. Choose the Right Algorithm
For new projects: Use Argon2id with these parameters:
- Memory cost: 64 MB (65536 KB)
- Time cost: 3 iterations
- Parallelism: 4 threads
For existing projects: Bcrypt with cost factor 12+ is still secure. Consider migrating to Argon2 if you're doing a major rewrite.
For enterprise/compliance: PBKDF2 with SHA-256, 100,000+ iterations.
2. Always Use Salts
Never hash passwords without salts. Modern algorithms handle this automatically, but if you're implementing manually:
- Use cryptographically random salts
- Unique salt per password
- Store salts alongside hashes
3. Set Appropriate Cost Factors
Test on your actual hardware. Aim for 200-500ms per hash:
// Node.js: Adaptive cost factor
const bcrypt = require('bcrypt');
async function benchmarkBcrypt() {
const testPassword = "testPassword123";
const start = Date.now();
await bcrypt.hash(testPassword, 12);
const duration = Date.now() - start;
// Aim for 200-500ms per hash
if (duration < 200) {
return 13; // Increase cost
} else if (duration > 500) {
return 11; // Decrease cost
}
return 12; // Optimal
}
4. Use Timing-Safe Comparison
Prevent timing attacks with constant-time comparison:
// Node.js: Timing-safe comparison
const crypto = require('crypto');
function timingSafeEqual(a, b) {
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
5. Implement Rate Limiting
Protect authentication endpoints:
// Example: Express.js rate limiting
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts, please try again later.'
});
6. Store Hashes Securely
- Never log password hashes
- Use environment variables for peppers
- Encrypt database backups
- Implement proper access controls
7. Plan for Algorithm Migration
Design your system to allow upgrades:
// Node.js: Algorithm-agnostic verification
async function verifyPassword(password, storedHash) {
// Detect algorithm from hash format
if (storedHash.startsWith('$2b$') || storedHash.startsWith('$2a$')) {
// Bcrypt
return await bcrypt.compare(password, storedHash);
} else if (storedHash.startsWith('$argon2id$')) {
// Argon2
return await argon2.verify(storedHash, password);
} else {
// Legacy or unknown - trigger rehash
throw new Error('Unsupported hash algorithm');
}
}
Real-World Implementation
Here's a complete authentication system you can use:
// auth.js - Complete password hashing implementation
const bcrypt = require('bcrypt');
class PasswordManager {
constructor(costFactor = 12) {
this.costFactor = costFactor;
}
async hashPassword(password) {
return await bcrypt.hash(password, this.costFactor);
}
async verifyPassword(password, hash) {
if (hash.startsWith('$2')) {
return await bcrypt.compare(password, hash);
}
throw new Error('Invalid hash format');
}
async needsRehash(hash) {
if (hash.startsWith('$2b$')) {
const rounds = parseInt(hash.split('$')[2]);
return rounds < this.costFactor;
}
return true; // Unknown format, should rehash
}
}
// Usage
const passwordManager = new PasswordManager(12);
async function registerUser(email, password) {
const hash = await passwordManager.hashPassword(password);
// Store in database
await db.users.insert({
email,
passwordHash: hash,
createdAt: new Date()
});
}
async function authenticateUser(email, password) {
const user = await db.users.findOne({ email });
if (!user) {
return false;
}
const isValid = await passwordManager.verifyPassword(
password,
user.passwordHash
);
// Update hash if needed (gradual migration)
if (isValid && await passwordManager.needsRehash(user.passwordHash)) {
const newHash = await passwordManager.hashPassword(password);
await db.users.update(
{ email },
{ passwordHash: newHash }
);
}
return isValid;
}
Common Mistakes (Don't Do This)
Using Fast Hash Functions
// WRONG - Too fast!
const hash = crypto.createHash('sha256').update(password).digest('hex');
Reusing Salts
// WRONG - Same salt for all passwords
const salt = 'fixedSalt123';
Storing Plaintext
// WRONG - Never do this!
await db.users.insert({ email, password: plaintextPassword });
Insufficient Cost Factors
// WRONG - Too low!
const hash = await bcrypt.hash(password, 4);
Testing Your Implementation
Use our tools to test and verify your implementations:
- Bcrypt Generator - Generate and verify bcrypt hashes with different cost factors
- Hash Generator - Test SHA-256, SHA-512, MD5 (for data integrity, not passwords!)
- Password Strength Checker - Ensure users create strong passwords before hashing
Pro tip: Test your password hashing implementation with our Bcrypt Generator to benchmark different cost factors and find the optimal balance between security and performance for your hardware.
Frequently Asked Questions
Is Bcrypt still safe in 2025?
Yes, Bcrypt remains secure with cost factor 12 or higher. Argon2id offers better hardware resistance, but Bcrypt is fine for most applications.
Bcrypt vs Argon2: Which should I use?
Use Argon2id for new projects (better hardware resistance). Bcrypt is still secure and may be preferable for compatibility or existing systems.
Is SHA-256 secure for passwords?
No, never use SHA-256 directly for passwords. It's too fast. Use it with PBKDF2, or use Bcrypt/Argon2 instead.
What is the best password hashing algorithm in 2025?
Argon2id is the best choice for new applications. It provides memory hardness, configurable parameters, and modern design.
How to store passwords securely?
- Use Argon2id (new) or Bcrypt (existing)
- Always use salts (automatic with modern algorithms)
- Set appropriate cost factors
- Implement rate limiting
- Use timing-safe comparison
- Plan for algorithm migration
What is hashing and salting passwords?
Hashing converts passwords into fixed-length strings that can't be reversed. Salting adds random data to each password before hashing, ensuring identical passwords produce different hashes.
Can I use MD5 for password hashing?
No, MD5 is cryptographically broken. Attackers can compute billions of MD5 hashes per second. Never use it for passwords.
What is memory hardness?
Memory hardness makes an algorithm require significant RAM to compute efficiently. Algorithms with memory hardness (Argon2, scrypt) resist GPU/ASIC attacks.
🔄 Updated for 2025
This guide has been updated to reflect the latest security recommendations and hardware capabilities in 2025.
Hardware Improvements
Modern GPUs and ASICs have become significantly more powerful since 2020. A single high-end GPU can now compute:
- SHA-256: Over 2 billion hashes/second (up from ~1 billion)
- Bcrypt (cost 10): ~500 hashes/second (up from ~200)
- Argon2id: Still resistant due to memory requirements
GPU Cracking Speeds
Attackers can now crack weak passwords faster than ever:
- SHA-256 passwords: Cracked in seconds (even with salts)
- Bcrypt (cost 8): Vulnerable to GPU attacks
- Bcrypt (cost 12+): Still secure, but consider upgrading to cost 13-14
New Argon2 Recommendations
The IETF has updated Argon2 recommendations for 2025:
- Memory cost: 64-128 MB (up from 32-64 MB)
- Time cost: 3-4 iterations (up from 2-3)
- Parallelism: 4-8 threads (depending on server capacity)
What This Means for You
- Existing Bcrypt systems: Increase cost factor to 13-14 if possible
- New projects: Use Argon2id with updated parameters
- High-security applications: Consider Argon2id with 128 MB memory cost
- Rate limiting: More important than ever—implement strict limits
Test your current setup: Use our Bcrypt Generator to benchmark your current cost factors and see if they meet 2025 security standards.
Conclusion
Password security in 2025 isn't complicated—it's about choosing the right algorithm and implementing it correctly.
The bottom line:
- Use Argon2id for new projects
- Use Bcrypt (cost 12+) for existing systems
- Never use SHA-256 or MD5 directly for passwords
- Always use salts (automatic with modern algorithms)
- Set appropriate cost factors based on your hardware
Stay secure, and happy coding!