Fast hashes are the problem, not weak ones
SHA-256 is not broken. No practical collision, no preimage attack, nothing wrong with it as a hash. It’s wrong for passwords for a different reason entirely: it was designed to be fast, and password hashing is the one job where speed works for the attacker.
The numbers make the point better than any argument. Hashcat benchmarks on a single Nvidia RTX 4090 (the widely cited v6.2.6 run published by Chick3nman in October 2022) clock in at roughly 164 billion MD5 hashes per second, 49 billion SHA-1, 21 billion SHA-256, and 288 billion NTLM. One gaming GPU. An attacker with a stolen hash database doesn’t attack your login form with its rate limits; they run these speeds offline against every hash at once.
At 21 billion guesses per second, every 8-character password made of lowercase letters and digits falls in under 90 minutes. Human-chosen passwords fall much faster, because crackers don’t brute-force blindly; they run wordlists from previous breaches with mutation rules, and humans keep choosing Summer2026!. The LinkedIn breach made this concrete: 2012, unsalted SHA-1, and when the full dump of 117 million accounts surfaced in 2016, the overwhelming majority were cracked within days.
So the defining property of a password hash is that it’s expensive on purpose, with a knob to make it more expensive as hardware improves. That knob is the whole story of the next three algorithms.
bcrypt: designed in 1999, still not embarrassing
bcrypt was published by Niels Provos and David Mazières at USENIX in 1999, under the title “A Future-Adaptable Password Scheme”. The future-adaptable part is the cost factor: bcrypt runs an expensive variant of the Blowfish key schedule 2^cost times, so cost 11 is twice the work of cost 10. A design from before Y2K that anticipated Moore’s law and survived it. Respect.
And it genuinely holds up against GPUs, at least compared to fast hashes. The same RTX 4090 that does 21 billion SHA-256 per second manages about 184 thousand bcrypt hashes per second, and that benchmark runs at cost factor 5, which nobody uses in production. At a realistic cost of 12 that’s roughly 1,400 guesses per second. From 21 billion down to 1,400: that’s the entire argument for slow hashing in two numbers.
The reason bcrypt resists GPUs is its constantly-mutating 4 KB internal state, which fits badly into GPU memory access patterns. But 4 KB is also its weakness: it’s a fixed amount, small by modern standards, and dedicated hardware keeps getting better at parallelising it. Which is why the field moved on to memory-hardness. bcrypt isn’t wrong today; it’s just no longer the best answer.
The 72-byte trap (ask Okta how that goes)
bcrypt has a gotcha that most tutorials skip: it only processes the first 72 bytes of input. Not characters, bytes. Everything after byte 72 is silently ignored in essentially every implementation, no error, no warning. A user with a 90-character passphrase can log in with just the first 72 bytes of it, and neither of you will ever notice. Multi-byte UTF-8 makes it worse, since an emoji-heavy passphrase hits the limit at far fewer characters.
Silently is the operative word, and in 2024 it produced a real authentication bypass. Okta’s security advisory from October 30, 2024 described the following: their AD/LDAP delegated authentication generated a cache key by running bcrypt over the concatenated string of userId + username + password. With a username of 52 characters or more, the password landed partly or entirely beyond byte 72, where bcrypt ignores it. Under the right conditions (a cached key from an earlier successful login, and the auth agent unreachable or under high load), a request with the long username and any password matched the cache. The bug shipped on July 23, 2024 and sat there for three months; the fix swapped bcrypt for PBKDF2 in that code path.
Two lessons, both cheap: never feed bcrypt anything other than the password itself, and if you accept long passphrases, either enforce the limit visibly or use an algorithm without one. Argon2 and scrypt take arbitrary-length input.
Cost factors: what the knob should read in 2026
Every slow hash ships with defaults, and defaults age badly. bcrypt’s common default of 10 was chosen when servers were a lot slower; OWASP now names 10 as the minimum work factor, not the target. Benchmark on your actual production hardware and pick the highest cost that keeps one hash in the low hundreds of milliseconds. Login happens once per session; 250 ms is invisible to users and miserable for crackers.
Remember the knob is exponential. Moving bcrypt from 10 to 12 quadruples the attacker’s bill. And bake in a plan to raise it later: store the cost with the hash (bcrypt’s $2b$12$... format does this for you, as do Argon2’s encoded strings) and rehash transparently on the next successful login whenever the stored cost is below your current target.
scrypt and Argon2: making memory the bottleneck
Colin Percival introduced scrypt in 2009, built for his Tarsnap backup service and later standardised in RFC 7914. His insight: CPU time is cheap to parallelise, memory bandwidth isn’t. scrypt fills a large buffer with pseudorandom data and then reads it back in an unpredictable order, so an attacker can’t just throw ten thousand GPU cores at it; each core needs its own fat slice of RAM, and the die space that would hold thousands of hashing units now holds memory instead. That property is called memory-hardness, and it changed what a cracking rig costs per guess.
Argon2 refined the idea and won the Password Hashing Competition in 2015, an open contest (2013 to 2015, 24 entries) run by a panel of cryptographers explicitly to replace the aging options. It comes in three flavours; the one you want is Argon2id, a hybrid that resists both GPU cracking and the side-channel attacks that pure Argon2d is exposed to. It was standardised in RFC 9106 in 2021 and has three independent knobs: memory (m), iterations (t) and parallelism (p). No input length limit, salt handled for you, wide library support. This is the default answer now.
Parameters that hold up today
The OWASP Password Storage Cheat Sheet is the reference most auditors will hold you to, and its current first-listed Argon2id configuration is m=19456 (19 MiB), t=2, p=1. It lists equivalent trade-offs with more memory and fewer iterations (46 MiB with t=1 is one of them), and notes these are minimums for constrained environments. If your servers can afford more memory per login, spend it; RFC 9106 itself suggests configurations up to 2 GiB for high-security uses. For scrypt, OWASP’s floor is N=2^17, r=8, p=1, which works out to 128 MiB per hash.
| Algorithm | Year | Hard on | Baseline setting | Length limit |
|---|---|---|---|---|
| Argon2id | 2015 (RFC 9106, 2021) | Memory + CPU | m=19456, t=2, p=1 | none |
| scrypt | 2009 (RFC 7914, 2016) | Memory + CPU | N=2^17, r=8, p=1 | none |
| bcrypt | 1999 | CPU (4 KB state) | cost 10 minimum | 72 bytes |
| SHA-256, salted | 2001 | nothing | don’t | none |
One warning about p on typical web backends: parallelism above 1 only helps if the hashing library actually runs threads, and in most server runtimes it just serialises. Tune m and t first.
Salts you know, peppers you should
Salts are table stakes and every algorithm above generates and stores them for you: a unique random value per user, stored in plaintext next to the hash, so identical passwords produce different hashes and precomputed tables are useless. If you’re concatenating salts by hand in 2026, you’re holding the library wrong.
A pepper is the less-known upgrade. It’s a single secret mixed into every hash, stored away from the database: an environment secret, a KMS key, an HSM if you’re fancy. The standard construction is HMAC-SHA256 over the password with the pepper as the key, then Argon2id over the result. The payoff is specific: the most common breach shape is a database-only leak (SQL injection, an unsecured backup, a stolen replica), and against that, peppered hashes can’t even begin to be cracked because the attacker is missing an input. For anything holding real user accounts, the plumbing is an afternoon, and we think it earns its keep.
So why exactly is salted SHA-256 still weak?
Because the salt answers a different question than the one that matters. A salt defeats precomputation and hash reuse across users. It does nothing about per-hash guessing speed, and speed is where the fight is decided: 21 billion salted SHA-256 guesses per second on one GPU versus a four-digit number for a properly tuned Argon2id or bcrypt. Iterating SHA-256 a few thousand times (the PBKDF2 approach) narrows the gap but still loses to GPUs, which is why NIST-compliant PBKDF2 setups need six-digit iteration counts and still land behind memory-hard designs.
Related reading: password storage is one half of account security, and how TOTP two-factor codes actually work is the other. And if you ever catch a codebase “protecting” credentials with base64, send the author our explainer on why base64 is not encryption; a decoder undoes it in milliseconds. For session tokens after login, we’ve also written up where to store JWTs.
Password storage questions
Is SHA-256 secure for password hashing?
No. SHA-256 is a perfectly good cryptographic hash, but it is designed to be fast, and fast is exactly what you don’t want for passwords. A single RTX 4090 computes about 21 billion SHA-256 hashes per second in hashcat, so an attacker with a stolen database can test enormous chunks of the realistic password space offline. Salting stops precomputed rainbow tables but does nothing about that raw speed. Use a deliberately slow, tunable algorithm instead: Argon2id, scrypt or bcrypt.
What is the best password hashing algorithm in 2026?
Argon2id is the current default recommendation, including in the OWASP Password Storage Cheat Sheet. It won the Password Hashing Competition in 2015, is standardised in RFC 9106, and is both memory-hard and tunable. bcrypt remains acceptable if Argon2 isn’t available on your platform, and scrypt sits in between. What matters more than the exact pick is that you use one of these three with sane parameters, not a general-purpose hash like SHA-256 or MD5.
What is the difference between bcrypt and Argon2?
bcrypt (1999) is CPU-hard: it makes each hash slow by repeating work, but it only needs 4 KB of memory, which modern GPUs can still parallelise reasonably well. Argon2 (2015) is memory-hard on top of that: each hash needs tens of megabytes of RAM, and memory is the one resource GPUs and ASICs can’t cheaply multiply across thousands of parallel cores. Argon2 also has no input length limit, while bcrypt silently truncates passwords at 72 bytes.
What bcrypt cost factor should I use?
At least 10, which is also what OWASP names as the minimum work factor. Each increment doubles the work, so cost 12 is four times slower than cost 10. The practical rule: pick the highest cost your servers can absorb at peak login traffic, typically the value that lands one hash in the low hundreds of milliseconds on your production hardware. The default of 10 dates back many years, so measure instead of trusting it blindly.
What is a pepper in password hashing?
A pepper is a secret value mixed into every password hash, stored somewhere other than the database, for example in a KMS, an HSM or an environment secret. Unlike a salt, which is unique per user and stored right next to the hash, the pepper is shared and secret. The point: an attacker who dumps only your database (SQL injection, stolen backup) can’t even start cracking, because the pepper is missing. The usual construction is an HMAC of the password with the pepper as key, fed into Argon2id or bcrypt.
Can bcrypt handle long passwords?
Only the first 72 bytes count. Anything beyond that is silently ignored by essentially every bcrypt implementation, so a 100-character passphrase and its first 72 bytes produce the same hash. Note that it is bytes, not characters: multi-byte UTF-8 input hits the limit sooner. If you want to allow arbitrary-length passphrases with bcrypt, the common workaround is pre-hashing with SHA-512 plus base64 before bcrypt, but that has its own pitfalls (null bytes, password shucking), so switching to Argon2id is the cleaner fix.
Should I rehash existing password hashes when I upgrade algorithms?
Yes, and you don’t need to wait for users to log in for the first layer. The standard trick is wrapping: take the stored weak hash (say SHA-256) and run it through Argon2id immediately, storing argon2id(sha256(password)). That upgrades every account at once. Then, on each successful login, when you briefly hold the plaintext password anyway, replace the wrapped hash with a clean argon2id(password). Facebook ran this kind of onion construction for years.