A data breach that exposes your users' passwords is a catastrophic event. If those passwords are stored as plaintext — or hashed with MD5/SHA — attackers can crack millions of them in minutes. Proper password hashing is the last line of defense. This guide explains what to use, why, and how.
Why Storing Plaintext is Criminal
If your database is breached and passwords are stored in plaintext, every user's password is immediately compromised. Since most users reuse passwords across services, a breach of your site can lead to breaches of their banking, email, and social media accounts.
Why MD5 and SHA Are Wrong for Passwords
MD5 and SHA-256 are general-purpose hash functions. They were designed to be fast — SHA-256 can hash billions of passwords per second on a modern GPU. That's exactly what you don't want for passwords.
Algorithm Hashes/Second (GPU) Time to crack 8-char password ───────────────────────────────────────────────────────────────────── MD5 200 billion Seconds SHA-256 10 billion Minutes bcrypt (cost 12) 5,000 Thousands of years Argon2id 1,000 Millions of years
What Makes a Good Password Hash?
- Intentionally slow: Takes 100ms-500ms per hash. Fast enough for login, too slow for brute force.
- Adjustable cost factor: As hardware gets faster, you increase the cost to keep hashing slow.
- Built-in salt: Each password gets a unique random salt, so identical passwords produce different hashes.
- Memory-hard (modern): Requires significant memory per hash, making GPU-based attacks impractical.
The Big Three: bcrypt, scrypt, Argon2
bcrypt (1999)
The industry standard for two decades. CPU-hard but not memory-hard. Still excellent and widely supported.
import bcrypt from 'bcryptjs';
// Hash a password (cost factor 12 — takes ~300ms)
const hash = await bcrypt.hash("userPassword123", 12);
// → "$2b$12$LJ3m4ys3Lz7g..."
// Verify a password
const isValid = await bcrypt.compare("userPassword123", hash);
// → true
Argon2id (2015 — Recommended)
Winner of the 2015 Password Hashing Competition. Both CPU-hard and memory-hard. The best option for new projects.
import argon2 from 'argon2';
// Hash a password
const hash = await argon2.hash("userPassword123", {
type: argon2.argon2id, // Recommended variant
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4 // 4 threads
});
// Verify
const isValid = await argon2.verify(hash, "userPassword123");
PHP's Built-in password_hash()
// Hash — uses bcrypt by default, Argon2id available in PHP 7.3+
$hash = password_hash("userPassword123", PASSWORD_ARGON2ID);
// Verify
if (password_verify("userPassword123", $hash)) {
echo "Password correct!";
}
// Check if hash needs rehashing (e.g., cost factor increased)
if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
$newHash = password_hash("userPassword123", PASSWORD_ARGON2ID);
// Update hash in database
}
What Is Salting?
A salt is a random string added to the password before hashing. Without salts, all users with the password "password123" get the same hash — attackers can use precomputed tables (rainbow tables) to crack them instantly. With salts, each hash is unique even for identical passwords.
Good news: bcrypt, scrypt, and Argon2 all handle salting automatically. You don't need to manage salts manually.
Best Practices
- Use Argon2id for new projects. Fall back to bcrypt if Argon2 isn't available.
- Never use MD5, SHA-1, or SHA-256 for password hashing.
- Never write your own hashing logic. Use battle-tested libraries.
- Increase the cost factor as hardware improves. Aim for 200-500ms per hash.
- Implement rate limiting on login endpoints to prevent online brute force.
- Support password managers — don't set maximum password lengths below 64 characters.
- Consider adding pepper — a secret key mixed into the hash, stored separately from the database.
Try Our Free Encoding Tools
Encode and decode Base64, used in HTTP authentication headers.