Why Passwords Need bcrypt, Not MD5: Cost Factor, Salt, and bcrypt vs argon2
A practical guide to bcrypt for password storage: why MD5 fails, how the cost factor and built-in salt work, a real hash walkthrough, and how bcrypt stacks up against argon2.
Why Passwords Need bcrypt, Not MD5
I once inherited a user table where passwords were stored as MD5(password). The database leaked, and within an afternoon a colleague had recovered roughly 40% of the plaintext passwords using a consumer GPU. That afternoon convinced me that the hashing algorithm you pick for passwords is not a detail — it is the whole game. This guide walks through why MD5 fails so badly, what the bcrypt cost factor and salt actually do, and where bcrypt sits next to argon2 in 2026.
MD5 and SHA Were Never Built for Passwords
MD5 and SHA-256 are designed to be fast. That is exactly what you want for checking file integrity, where you might hash gigabytes per second. It is exactly what you do not want for passwords. A modern GPU can compute billions of MD5 hashes per second, so an attacker holding a leaked table can simply try every word in a dictionary, every common password, and every leaked-credential combo — and check each guess almost for free.
Password hashing needs the opposite property: it should be deliberately, tunably slow. bcrypt was published in 1999 specifically for this. Its Eksblowfish key schedule runs an expensive setup step over and over, so the attacker is forced down to roughly the same speed as your own login server. They cannot buy their way around the math the way they can with a fast hash.
You can feel the difference yourself in the bcrypt generator: hashing one password at cost 14 takes about a second on a laptop, while an MD5 of the same string is instantaneous. That second is not a bug — it is the entire defense.
The Cost Factor: One Number That Doubles the Work
bcrypt's slowness is controlled by a single integer called the cost factor (or work factor). The relationship is exponential: bcrypt runs 2^cost iterations of its expensive round. Cost 10 means 1,024 iterations; cost 11 means 2,048; cost 14 means 16,384. Every time you add 1 to the cost, you double the time it takes to compute a hash — for you and for the attacker.
This is the lever that keeps bcrypt relevant 27 years later. As hardware gets faster, you simply raise the cost. OWASP currently recommends a minimum cost of 10 for bcrypt, and that remains a sane default for most web apps. The practical rule I use: pick the cost that lands a single hash around 250–500 ms on the slowest hardware that will run your auth code. On modern x86 servers that usually means cost 11 or 12.
Be careful at the top end, though. I have watched a team bump production to cost 14 "to be safe," then watch login latency cross a full second per attempt — a burst of sign-ins queued up and the auth endpoint fell over. Benchmark before you bump. Generate at cost 10, 12, and 14 on your real hardware and watch the timer before committing the number to config.
Salt: Why the Same Password Hashes Differently Every Time
Here is a property that trips people up. Hash the password hunter2 twice with bcrypt and you get two completely different strings. That is intentional. bcrypt mixes a fresh 128-bit random salt into every hash, so identical passwords never collide into identical hashes.
Why does that matter? Without a salt, an attacker can precompute a giant table of hash → password (a rainbow table) once and reuse it against every leaked database forever. A unique random salt per password defeats that entirely: the attacker has to attack each hash individually. It also hides the embarrassing fact that two users picked the same password.
The clever part is where bcrypt stores the salt. Look at a real hash:
password: correct horse battery staple
cost: 12
hash: $2b$12$Eq3p9qZ1mK0sX4dV7nBwLeK2hJ4uF8gT1cW6yR0aP5sD9zX2bN3O
That string carries everything verification needs. $2b$ is the algorithm version, 12 is the cost, the next 22 characters are the base64-encoded salt, and the rest is the actual hash output. Because the salt and cost travel inside the string, verifying a password only needs the password plus the full hash — no separate salt column. A common mistake is storing a "backup" salt column anyway; it is dead weight, and if it ever drifts out of sync with the hash, your verification breaks.
A Real Walkthrough: From Password to Verified Hash
Let me make this concrete. Say you are seeding an admin account into a fresh staging database. You type ChangeMe2026! into the generator, pick cost 12 to match your app config, and get back something like $2y$12$.... You paste that directly into your seed SQL. On first boot, the login form hashes whatever the admin types and compares — and because the salt and cost are baked into the stored string, the comparison just works.
Now flip to verify mode. A pentest dump hands you a $2a$10$ hash and you suspect a developer reused password123. Paste the candidate password and the hash, and in about 80 ms at cost 10 you get a clean true/false — no throwaway Node script required. If it matches, you file the finding with proof. If it does not, you move on instead of guessing. If you also need to compare bcrypt against the older fast hashes for a migration audit, the MD5 / SHA hash tool sits right next door for side-by-side checks.
bcrypt vs argon2 (and scrypt): Which to Choose in 2026
The honest answer: if you can pick freely, reach for argon2id first. It won the 2015 Password Hashing Competition and is OWASP's top recommendation today. Its big advantage is being memory-hard — it forces the attacker to spend RAM as well as CPU, which blunts the GPU and ASIC attacks that make MD5 cracking so cheap. argon2 lets you tune memory, iterations, and parallelism independently.
So why does bcrypt still show up everywhere? Two reasons. First, ubiquity — almost every language and framework ships a battle-tested bcrypt, while argon2 sometimes means an extra native dependency. Second, maturity: bcrypt has 27 years of cryptanalysis with no practical breaks. It is the safe, boring choice, and "boring" is a compliment in security. scrypt is also memory-hard and fine, but it has seen less review in the password context specifically.
One bcrypt gotcha to remember regardless: it truncates inputs past 72 bytes. A 90-character passphrase and its first 72 characters produce the same hash. If you accept long passwords, pre-hash with SHA-256 first, then feed that to bcrypt.
My rule of thumb: new greenfield project with argon2 available, use argon2id. Existing codebase, a framework that defaults to bcrypt, or a platform without argon2 — bcrypt at a benchmarked cost is completely defensible. The disaster is not "bcrypt instead of argon2." The disaster is fast hashes like MD5, no salt, and a leaked table on a cracking rig.
Practical Next Steps
If you are still on MD5 or SHA, migrate on a rolling basis: on each user's next login, re-hash their plaintext with bcrypt and replace the old value. For accounts that rarely log in, you can wrap the existing hash — bcrypt(sha256(password)) — as a stop-gap and upgrade to plain bcrypt later. Pair that with a real strength check at signup using the password strength checker, because the strongest hash in the world cannot save password123. Get the algorithm, the cost, and the salt right, and a future database leak becomes an inconvenience instead of a catastrophe.
Made by Toolora · Updated 2026-06-13