⚠️ Read this first. This article covers security-sensitive material. It is educational — not a substitute for a professional security review. Test every code sample in an isolated development environment before deploying it to a system that handles real user data. For production payment, authentication, or compliance work, hire a qualified security professional.

If you have ever stored a password as plain text or as a plain SHA-256 hash, this article is a critical read. Modern password hashing is one of the cheapest, easiest, and most impactful security improvements you can make to any application that handles user accounts. We will cover the algorithms, the parameters, the patterns, and the mistakes — everything you need to do this right.

What password hashing actually is

A password hash is a one-way function: easy to compute, hard to reverse. When a user signs up, you hash their password and store the hash. When they log in, you hash the candidate password and compare. If the hashes match, the password was correct. If the database is stolen, the attacker gets hashes — which are useless if the hashing algorithm was chosen well.

The "chosen well" part is where most developers go wrong. The hash function must be slow on purpose, so brute-force attacks are expensive. Plain SHA-256 is fast — too fast. A modern GPU can compute billions of SHA-256 hashes per second. bcrypt and Argon2 are deliberately slow, making brute force thousands of times more expensive.

Why plain SHA is a crime

SHA-256, SHA-1, MD5, and similar cryptographic hashes were designed to be fast. That is great for verifying file integrity, terrible for passwords. An attacker with a stolen database can try every common password in seconds. Even worse, attackers publish rainbow tables — precomputed hash-to-password lookups for billions of common passwords.

There is no good reason to use plain SHA for passwords in 2026. Every modern language has bcrypt or Argon2 in its standard library or ecosystem. There is no excuse.

bcrypt: the dependable default

bcrypt has been the standard password hash since 1999. It is built on the Blowfish cipher, intentionally slow, and easy to use:

// PHP
$hash = password_hash($password, PASSWORD_BCRYPT);

# Node.js
const bcrypt = require("bcrypt");
const hash = await bcrypt.hash(password, 12);

# Python
import bcrypt
hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt())

# C#
var hash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);

All four examples do the same thing: take a plain-text password, hash it with bcrypt, return the resulting string. The string includes the salt, the algorithm version, and the work factor, so you can verify against any bcrypt hash with one call:

// any language
const ok = await bcrypt.compare(candidate, stored);

The 12 in the examples above is the work factor — a measure of how slow the hash is. Higher means slower hashing and stronger resistance to brute force, at the cost of CPU time during login. 12 is a reasonable default in 2026.

Argon2: the modern winner

Argon2 won the Password Hashing Competition in 2015 and is the recommended choice for new applications. It has three variants:

  • Argon2d — resistant to GPU cracking, but vulnerable to side-channel attacks.
  • Argon2i — resistant to side-channel attacks, slower.
  • Argon2id — the hybrid; recommended for most uses.

Argon2 has three tunable parameters: time cost (iterations), memory cost (how much RAM the hash requires), and parallelism (how many threads). Reasonable defaults in 2026:

# Python
import argon2
ph = argon2.PasswordHasher(
  time_cost=3,        # iterations
  memory_cost=65536,  # 64 MiB
  parallelism=4
)
hash = ph.hash(password)
ph.verify(hash, password)

The memory cost is the killer feature: it makes GPU-based cracking much harder because each guess needs a full 64 MiB of memory.

How to pick

For new applications, use Argon2id. For existing applications, bcrypt is still excellent and migration is straightforward. Avoid:

  • Plain SHA-1, SHA-256, MD5. Way too fast. Vulnerable to brute force.
  • Single-round SHA with a static salt. Defeats rainbow tables but still fast.
  • Custom homegrown algorithms. Almost certainly broken. Use a vetted library.
  • Encryption instead of hashing. Encryption is reversible. Hashing is not. Passwords should never be reversible.

Salting: what it is and why it matters

A salt is a random value added to the password before hashing, so two users with the same password get different hashes. Without a salt, an attacker can precompute hashes for common passwords once and look them up across the entire database. With a salt, every hash is unique even for the same password.

All modern password hashing functions (bcrypt, Argon2, scrypt) handle salt generation automatically. The library generates a random salt, mixes it into the hash, and stores it as part of the resulting string. You should never manage salts yourself.

Verification: constant-time comparison

When you check a candidate password, you must use a constant-time comparison function. A naive hash == candidate check is vulnerable to timing attacks: an attacker can measure how long the comparison takes and infer how many leading characters matched. The libraries handle this for you:

bcrypt.compare(candidate, storedHash)  // safe
argon2.PasswordHasher().verify(storedHash, candidate)  // safe

Never write your own comparison. Always use the library's verify function.

Migrating from a bad hash

If you have an existing database of plain SHA hashes, you can migrate transparently. On next login, verify the user against the old hash. If it matches, rehash with bcrypt/Argon2 and store the new hash. From that point on, the user is on the modern algorithm. Over time, every active user migrates without a forced password reset.

def verify_and_migrate(candidate, stored):
    if sha256_matches(candidate, stored):  # legacy check
        new_hash = argon2.hash(candidate)
        db.update_password(user_id, new_hash)
        return True
    return argon2.verify(stored, candidate)

This pattern is widely used in production. Do not require users to reset their passwords during the migration; you will lose a chunk of them.

Work factor tuning

Both bcrypt and Argon2 let you tune the work factor over time. As CPUs get faster, you can increase the factor. Always store the factor (or the full hash, which includes it) so you can verify against old hashes while new ones use the higher factor.

A good test: hashing should take between 250ms and 1 second on your production server. Faster than that and you should raise the work factor. Slower than that and your login endpoint will feel sluggish.

Common pitfalls

  • Hashing with a fast function. Use bcrypt or Argon2, not SHA-256.
  • No salt. All modern libraries handle this. Just make sure you are using a current library.
  • Custom comparison logic. Use the library's verify function.
  • Logging passwords. Never log the plain-text password. Even in error paths. Especially in error paths.
  • Hashing in the browser. Password hashing should always happen server-side. Client-side hashing does not protect the password during transit.
  • Emailing plain-text passwords. Never, ever, ever. If your sign-up flow emails the user's password, your flow is broken.

A note on rate limiting and breach response

Even with perfect hashing, an attacker can still try to log in by guessing passwords. Rate limiting on your login endpoint (5 attempts per minute per IP, with exponential backoff) is essential. After 10 failed attempts, lock the account and require an email-based reset. Do not silently swallow attempts; log them and alert on suspicious patterns.

If a breach occurs, rotate every secret, force a password reset for affected users, and publish a clear, honest disclosure. Pretending nothing happened is the worst possible response. Users can protect themselves if you tell them what happened.

Further reading

Password hashing is one of the few areas where getting it wrong has permanent, irreversible consequences for your users. These are the references we use.

FAQ

What about password managers and passkeys?

They are the future. Passkeys (based on WebAuthn) replace passwords entirely with cryptographic key pairs. The server stores a public key; the client proves possession of the private key. No password to steal. Support is growing fast but not universal, so traditional password hashing will remain necessary for years.

Should I require special characters in passwords?

Length matters more than complexity. The National Institute of Standards and Technology (NIST) recommends a minimum of 8 characters, with no required character classes. Longer passphrases ("correct horse battery staple") are stronger than shorter complex ones ("P@ssw0rd!"). Encourage length, allow spaces.

What is a pepper?

A pepper is a secret value added to the password before hashing, stored separately from the database (often in an environment variable). It defends against database-only breaches. Use carefully — losing the pepper invalidates every password. Most apps do not need one.

How often should I rotate passwords?

For users, almost never. Forced rotation leads to weaker passwords and forgotten accounts. For service accounts and API keys, on a defined schedule (90 days is common) and on any suspected compromise.

What about hashing the hash?

Not necessary if you are using bcrypt or Argon2 with appropriate work factors. Modern algorithms already do many internal rounds.

Is double hashing better?

No. It does not meaningfully increase security and can introduce bugs. Use a single call to a vetted algorithm with appropriate parameters.

What about quantum computers?

Current password hashes are not directly threatened by quantum computing — they rely on the difficulty of reversing a hash, not factoring primes. Grover's algorithm provides a quadratic speedup on brute force, effectively halving the work factor. Bump your work factor to compensate.

Audit your code carefully and do not skip the migration step.

If you are tempted to add any of the discouraged options above, take a break and read the OWASP Authentication Cheat Sheet first. The discipline pays off.

Take it slow, test in isolation, and never rush a security change.

Take your time and verify every change with proper testing.

Test every change before deploying to production.

Audit, plan, and ship carefully.

Read everything carefully.

Take your time, test thoroughly, and never let a deadline push you into cutting security corners.

Homework

Replace the password storage in a small app with proper hashing:

  • Audit your current password storage. If it is plain text or SHA-anything, fix it today.
  • Add bcrypt or Argon2 to your sign-up flow. Hash the password before storing.
  • Add the corresponding verify function to your sign-in flow.
  • If you have legacy hashes, implement the verify-and-migrate pattern from this article.
  • Tune the work factor so login takes 250ms to 1 second on your hardware.
  • Add a unit test that verifies wrong passwords are rejected.

Once you have done this once, you have the muscle memory for every project that follows. For the JWT side of authentication, see our JWT Authentication article.

Security note: Password hashing is a YMYL topic. The recommendations in this article reflect industry best practice in 2026, but cryptography is a moving target. Do not treat this article as a substitute for a security audit by a qualified professional. If you are protecting user data, consult a security engineer.