Password Hashing Explained:bcrypt vs Argon2
When your database leaks, and you should assume one day it will, the difference between a minor incident and a disaster is how you stored the passwords. Store them well and the stolen file is nearly useless. Store them badly and attackers crack millions of accounts in hours, then use those passwords to attack every other site your users visit. This guide explains password hashing in plain language and compares the two functions you should actually use: bcrypt and Argon2.
Why Can You Not Just Store Passwords Directly?
Storing passwords as plain text means anyone who reads your database sees every password instantly. That includes attackers, but also employees and anyone who gets hold of a backup. The fix is hashing: run the password through a one-way function and store only the result. You can check a login by hashing the entered password and comparing, but you can never turn the stored hash back into the original. The catch is that not all hashes are suitable. Fast hashes designed for files, like MD5 and SHA-256, are the wrong tool for passwords.
Why Are MD5 and SHA-256 Wrong for Passwords?
MD5 and SHA-256 were built to be fast, which is exactly the problem. Modern hardware can compute billions of SHA-256 hashes per second, so an attacker with a leaked database simply hashes every common password and every entry in a wordlist and compares. Speed that is a feature for checksums is a gift to a password cracker. Password hashing functions do the opposite on purpose: they are slow and use a lot of memory, so each guess costs the attacker real time and hardware. That is the whole idea behind bcrypt and Argon2.
| Approach | Safe for Passwords? | Why |
|---|---|---|
| Plain text | No | Anyone who reads the database sees every password. |
| MD5 / SHA-1 / SHA-256 | No | Too fast. Attackers crack billions of guesses per second. |
| bcrypt | Yes | Deliberately slow with a tunable cost factor. Widely supported. |
| Argon2id | Yes | Slow and memory-hard. Resists GPU and custom-hardware cracking. |
What Is a Salt and Why Does It Matter?
A salt is a random value added to each password before hashing, and stored alongside the hash. Its job is to make sure two users with the same password get different hashes. Without salts, an attacker can precompute a giant lookup table (a rainbow table) once and use it against every leaked database, and can instantly see which users share a password. With a unique salt per password, those shortcuts stop working: the attacker has to crack each account separately. The good news is that bcrypt and Argon2 generate and store the salt for you, so you do not manage it by hand. Just never reuse a salt and never use a fixed one.
bcrypt vs Argon2: Which Should You Choose?
Both are correct choices that OWASP recommends. The difference is how they make cracking expensive and how you tune them.
| Feature | bcrypt | Argon2id |
|---|---|---|
| Slows attackers by | CPU cost (a work factor) | CPU and memory cost together |
| Resists GPU cracking | Somewhat | Strongly, because memory is hard to parallelise |
| Tuning knobs | One (cost factor) | Three (memory, iterations, parallelism) |
| Password length limit | 72 bytes, longer input is truncated | No practical limit |
| Best for | Mature ecosystems and wide library support | New projects wanting the strongest default |
The practical guidance: choose Argon2id for a new project if a well-maintained library is available for your language. Choose bcrypt if you want the most battle-tested and universally supported option, or if you are already using it. Both are far better than any fast hash. One bcrypt gotcha to know: it only reads the first 72 bytes of the input, so very long passwords are silently cut. A common fix is to pre-hash long inputs, or simply prefer Argon2id where length matters.
How Do You Hash a Password in Code?
You should almost never implement the algorithm yourself. Use the vetted library for your language and let it handle the salt and encoding. Here is the shape of it in Node.js.
// Argon2id: sign up, then verify at login
import argon2 from 'argon2';
// On signup: hash and store the result (salt is included)
const hash = await argon2.hash(password, { type: argon2.argon2id });
// On login: verify the entered password against the stored hash
const ok = await argon2.verify(hash, enteredPassword);
if (ok) allowLogin();
How Do You Store Passwords Safely: OWASP Checklist
- 1Use Argon2id or bcrypt, never MD5, SHA-1, SHA-256, or plain text.
- 2Let the library generate a unique salt per password and store it with the hash. Never reuse or hardcode a salt.
- 3Tune the cost so a single hash takes roughly 250 to 500 milliseconds on your production hardware.
- 4For bcrypt, remember the 72-byte input limit and pre-hash longer passwords if needed.
- 5Block breached and common passwords at signup with the HaveIBeenPwned Pwned Passwords API before you hash.
- 6Rehash and upgrade a user's password to stronger parameters the next time they log in successfully.
- 7Store the hash in a database column that is never returned by default queries or logged.
- 8Consider adding a server-side secret (a pepper) held outside the database for defence in depth.
What Does OWASP Say About Password Storage?
The OWASP Password Storage Cheat Sheet is direct: use Argon2id as the first choice, with parameters of at least 19 MiB of memory, and fall back to bcrypt with a work factor of 10 or more where Argon2 is not available. It stresses salting every password, tuning the cost to your hardware, and rehashing when you raise the parameters. NIST guidance aligns with this and adds that you should check new passwords against breach corpora and avoid forced periodic resets. In short: a slow salted hash plus breached-password blocking is the baseline for storing credentials safely.