The Goal

Even if your database leaks, attackers should not be able to recover users' passwords. This is the entire point of password hashing.

Plaintext passwords in a database is malpractice. It means a single SQL injection or backup leak hands every password to the attacker, and since people reuse passwords, they now have access to every account those users have on every other site.

Why Regular Hashes Aren't Enough

You might think "just use SHA-256." Wrong. Two reasons:

SHA is fast. Modern GPUs compute billions of SHA-256 hashes per second. An attacker with a leaked password database can try 100 billion guesses per hour against your password list.
No salt. Identical passwords produce identical hashes. Rainbow tables (precomputed lookups) exist for SHA. Lookup any common password instantly.

You need something slow and unique per user.

The Right Algorithms

Three algorithms are considered safe today:

Argon2id (the modern winner). Memory-hard. The most resistant to GPU and ASIC attacks. Use this for new systems.
bcrypt. The veteran. Still safe. Widely supported. Has been the default for two decades.
scrypt. Memory-hard predecessor to Argon2. Still acceptable.
PBKDF2. Marginal. Acceptable in regulated environments where it's the only approved option.

Avoid: MD5, SHA-1, SHA-2 (without a slow KDF wrapping them), unsalted hashes, anything you read about on a blog from 2010.

How These Algorithms Work

The pattern:

1. Generate a random salt per password (16+ bytes).
2. Combine salt + password.
3. Run through a slow function, deliberately doing thousands or millions of iterations or using lots of memory.
4. Store the salt + parameters + hash output.

Verification: take the user-provided password, the stored salt, run through the same function with the same parameters, compare the output to the stored hash.

Why slow? An attacker brute-forcing has to do the same slow operation for every guess. If a single hash takes 100ms, that's a few hundred thousand guesses per second per machine instead of billions.

Salts

A salt is random data appended to the password before hashing. Two effects:

Identical passwords produce different hashes (different salts).
Rainbow tables don't work (would need a separate table per salt).

Salts are not secret; they're stored alongside the hash. They just need to be unique per user.

Peppers

Optional extra layer. A pepper is a single secret value (not stored in the database) that's combined with the password during hashing. Even if the database leaks, attackers still need the pepper to brute-force.

Trade-off: peppers add operational complexity (key management, rotation). Use Argon2id with strong parameters first; consider a pepper for high-stakes systems.

Choosing Parameters

The algorithms have tunable parameters: iteration count, memory, parallelism. The right values depend on your hardware and how slow you can afford the verification to be.

Rough targets:

Argon2id: 3 iterations, 64MB memory, 4 parallelism. Should take ~250-500ms.
bcrypt: cost factor 12 (which means 2^12 iterations). Should take ~250ms.

Tune until verification takes long enough to be slow for attackers but short enough that legitimate users don't notice. Re-tune every few years as hardware improves.

Implementation Tips

Don't roll your own. Use the language's well-vetted library (bcrypt, libsodium, argon2-cffi, Spring Security).
Constant-time comparison. Compare hashes with constant-time functions to avoid timing attacks. Most libraries do this for you.
Versioned hashes. Store the algorithm and parameters with the hash so you can upgrade later.
Rehash on login. When a user logs in successfully, check if the hash uses old parameters. If so, rehash with new parameters and store. Gradually upgrades the database.

Beyond Hashing

Password hashing is necessary but not sufficient. Also:

Rate limit login attempts. Block after N failures.
Require strong passwords. NIST guidelines: 8+ characters, allow long phrases, no forced complexity rules.
Check against breach databases. Have I Been Pwned has an API for checking known-bad passwords.
Enable MFA. Even a perfect password is one factor; add another.
Use a password manager. Tell users. Better still, support passkeys.

Passkeys: The Future

Passwords are inherently risky. Passkeys (a WebAuthn-based standard) replace them with public-key cryptography handled by the user's device. The server stores a public key; only the device with the matching private key can authenticate.

If you're building a new system in 2026, support passkeys alongside passwords. They eliminate phishing, brute-force, and reuse attacks.

The One Thing to Remember

Use Argon2id (or bcrypt if your stack only supports it). Generate a unique salt per user. Tune parameters to be slow enough that brute-force is impractical. Use a battle-tested library. Layer in rate limiting, MFA, and passkeys. Don't write password hashing code yourself; you will get it wrong.