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.

Speed Comparison
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.

bcrypt in Node.js
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.

Argon2 in Node.js
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()

PHP Password Hashing
// 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.