Why password hashing is its own discipline
Storing passwords is not the same problem as hashing a file. A checksum wants to be as fast as possible; a password hash wants to be as slow as you can tolerate. The threat model is a stolen database: once an attacker has your table offline, the only thing standing between them and your users' credentials is how many guesses per second the algorithm permits, multiplied by how predictable the passwords are.
That is why purpose-built password hashes have three properties a plain digest lacks: a deliberate work factor, a mandatory per-record salt, and often memory-hardness that removes the advantage of massively parallel GPU and ASIC hardware. The generator above makes the gap concrete — bcrypt at cost 12 takes noticeable time to run in your browser, while the salted SHA-256 of the same password appears instantly. An attacker experiences exactly that ratio, multiplied across billions of attempts.
Choosing an algorithm in 2026
| Algorithm | Recommended parameters | Verdict |
|---|---|---|
| Argon2id | 19 MiB memory, t=2, p=1 | First choice |
| bcrypt | cost 12, 72-byte input limit | Excellent, ubiquitous |
| scrypt | N=2^17, r=8, p=1 | Good |
| PBKDF2-SHA256 | 600,000 iterations, 16-byte salt | Acceptable / FIPS |
| Salted SHA-256 | — | Not for passwords |
| Salted MD5 / SHA-1 | — | Migrate away now |
A storage schema that ages well
Store the algorithm and its parameters with every hash rather than assuming them globally. Modular crypt format does this in one string — $2b$12$… or $argon2id$v=19$m=19456,t=2,p=1$… — which is why a single VARCHAR(255) column is usually all you need. Self-describing hashes let you raise the work factor, or change algorithm entirely, without a flag day.
CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email CITEXT UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, -- full modular crypt string hash_updated TIMESTAMPTZ NOT NULL DEFAULT now() );
Upgrading legacy hashes without locking anyone out
- Add the algorithm marker to existing rows so you can tell an old MD5 record from a new bcrypt one.
- On successful login, verify with the legacy scheme, then re-hash the plaintext you already hold in memory with the new one and overwrite the row in the same transaction.
- Announce a migration window. When it closes, invalidate the remaining legacy rows and send those accounts through password reset.
- Never attempt a bulk conversion by hashing old hashes — nesting bcrypt around MD5 keeps you tied to MD5's weaknesses forever and makes eventual cleanup harder.
Around the hash
Hashing correctly is necessary but not sufficient. Rate-limit and back off on failed logins per account and per IP. Screen new passwords against known-breached lists rather than imposing composition rules that push people toward Password1!. Follow NIST SP 800-63B: allow long passphrases, accept every printable character including spaces, and drop mandatory periodic expiry. Offer TOTP or passkeys, and make password reset tokens single-use and short-lived. A perfect bcrypt configuration will not save an account whose reset link never expires.