Security Guides
How to store passwords safely — the right way to hash and salt
How should you store user passwords? Why plaintext, encryption, and raw hashes all fail, what a per-user salt does, and the real answer — a slow hash (bcrypt/Argon2/scrypt): how to choose it, tune it, and migrate an existing system. Defensive and developer-focused, no attack steps.
"How exactly should I store user passwords in the database?" — every builder hits this question once. The answer is clear. Here's the safe way to store them, in order, with no attack steps.
Why plaintext, encryption, and raw hashes all fail
Assume the database leaks someday. When it does, the damage differs wildly by storage method.
- Plaintext: every password is exposed the instant it leaks. Worse, password reuse chains the breach into the user's other services. The worst case.
- Encryption (reversible): the key turns it back — so if the key leaks alongside it, you're back to plaintext. A password never needs reading back, so being reversible buys you nothing.
- Raw hash (MD5/SHA-256): a fast hash lets an attacker test guesses rapidly. Common passwords fall to rainbow tables and brute force.
The "four stages" to safe
It's quickest to understand as adding one fix at a time to a weak method.
The key insight: a salt and a slow hash do different jobs. A salt defeats precomputation (rainbow tables) and bulk reuse cracking. A slow hash drops brute force to an impractical rate. You need both before it's safe (→ what hashing is).
In practice: what to do now
Use Argon2id (or bcrypt)
For new systems, make Argon2id the first choice — it can demand memory too, which resists GPU brute force. If you favor a battle-tested implementation, bcrypt is plenty in practice. Use the standard library implementation as-is for either.
Let the salt be automatic
bcrypt/Argon2 generate a per-user salt and embed it inside the hash string. You don't keep a separate salt column. Hand-building MD5(salt + password) is the classic thing not to do.
Tune the cost for your environment
Set bcrypt's cost factor, or Argon2's memory/iterations/parallelism, as high as you can while a legitimate login still feels instant. As servers get faster, so do attackers — revisit and raise it yearly.
Verify with the standard compare function
At login, match the input against the stored hash with the library's verify function (most use a timing-safe constant-time compare). Don't write your own string equality.
Migrate weak hashes by re-hashing on login
If you already store MD5/SHA-256, the moment a user logs in successfully, re-hash with the new scheme and save it. For users who haven't logged in, you can interim-upgrade by wrapping the existing hash inside bcrypt (a layered hash).
Common mistakes vs the right implementation
Common mistake
- Encrypt passwords for storage (key leaks → over)
- Store
MD5(password)orSHA-256(password)directly - Use a single shared salt for all users
- Hand-roll
hash(salt + password)
Right implementation
- One-way hash with Argon2id / bcrypt
- A per-user salt (the library adds it automatically)
- Cost as high as feels OK, raised periodically
- Use the standard hash/verify functions as-is
This site's view: password storage is a place to ride the boring, proven answer
Password storage is not a place to be creative. Riding the world-tested standard implementations of Argon2/bcrypt is the safest and the easiest to operate. Our stance: don't innovate here. Spend your effort instead on reducing dependence on passwords at all — thorough multi-factor authentication (MFA), and an eventual move to passkeys (passwordless). Even the strongest hash can't save a weak, reused password.
Read next
- Glossary: what password hashing is / what a salt is
- Basics: storing passwords the right way (user side) / choosing a password manager
- Defense: choosing MFA the right way (reduce password dependence)
FAQ
QShould I just encrypt passwords for storage?
No — encryption is the wrong tool. Encryption is reversible with a key, so if the key leaks, every password is back to plaintext. A password never needs to be read back, even by you, so an irreversible 'hash' is the right answer. But a raw hash isn't enough; combine a per-user salt with a slow hash (bcrypt/Argon2).
Qbcrypt or Argon2 — which should I use?
For new development, Argon2 (Argon2id especially) is the first choice: it lets you tune both memory and compute, which resists GPU brute force well. If you value a long-established, battle-tested implementation, bcrypt is still perfectly practical. The key point with either is to use the standard library implementation as-is and not roll your own.
QI already stored passwords with MD5/SHA-256. How do I migrate?
You can't bulk-convert back to plaintext (that's the point), so migrate on login. The moment a user logs in successfully, re-hash the correct password they just entered with the new scheme (e.g. Argon2id) and save it. For users who haven't logged in, you can interim-upgrade by wrapping the existing hash inside bcrypt (a layered hash).