How password hashing works, and why slow is the point
Every property that makes a hash function good at checksums makes it bad at passwords. Bcrypt and Argon2 are deliberately, expensively slow, and that is the entire feature.
Storing passwords is a solved problem, and it is solved in a way that looks wrong at first glance: the correct algorithm is chosen specifically for being slow, and it is tuned to get slower as computers get faster.
The progression of bad ideas
Plaintext. A breach hands over every account immediately, and because people reuse passwords, it hands over their email and banking too. Still found in the wild.
A plain hash — MD5, SHA-1, SHA-256. Better, and not sufficient. These are built for speed: a modern GPU computes billions of SHA-256 hashes per second. An attacker with your hash table does not reverse the hashes; they guess. Every word in every leaked password list, every dictionary word with every common substitution, run through the same function and compared. Most user passwords fall in minutes.
A plain hash without a salt is worse still, because the work is reusable. Precomputed tables mapping common passwords to their hashes — rainbow tables — exist for every fast hash, so the attack is a lookup rather than a computation. And identical passwords produce identical hashes, so a breach immediately reveals which users share a password.
Salting
A salt is a random value, unique per user, mixed into the password before hashing and stored alongside the result.
It fixes both problems above. Precomputed tables become useless, since a table would have to exist for every possible salt. And two users with the same password now have entirely different hashes, so nothing is learnable by comparison.
What a salt does not do is slow down an attack on one specific password. If the attacker has the salt — and they do, it is stored with the hash — they can still guess against that user at full speed. The salt forces the work to be done per user instead of once for everyone. That is valuable and it is not sufficient on its own.
Salts do not need to be secret, only unique and random. Modern password hashing functions generate and embed the salt for you; you should never be writing salt-handling code yourself.
Making it slow on purpose
This is the part that inverts normal engineering instincts.
A password check happens once, when someone logs in. Taking 250 milliseconds is imperceptible to them. An attacker needs to try billions of candidates — and at 250 ms each, billions of guesses becomes centuries.
So password hashing functions are built with a work factor you set. Raise it and every hash costs more CPU. The user notices nothing; the attacker's timeline multiplies.
Crucially, the work factor is adjustable, because hardware keeps improving. A cost that took 100 ms in 2015 takes a fraction of that now. The setting is meant to be revisited — the usual guidance is to tune it so hashing takes roughly 200–500 ms on your production hardware, and to raise it every few years.
The algorithms
bcrypt — from 1999, still entirely respectable. Its cost factor is logarithmic: cost 12 is twice the work of cost 11. Cost 12 is a reasonable 2026 default, around 200–300 ms on typical server hardware. Its one real limitation is that it silently truncates input beyond 72 bytes, which matters if you allow long passphrases — everything past character 72 is ignored, and no error is raised.
scrypt — adds memory hardness. bcrypt is CPU-expensive but uses little memory, and specialised hardware can parallelise it cheaply. scrypt requires a configurable amount of RAM per hash, which makes massive parallelism expensive rather than merely slow.
Argon2 — winner of the 2015 Password Hashing Competition and the current recommendation. Three parameters: time cost, memory cost and parallelism. Use the Argon2id variant, which combines resistance to side-channel attacks and to GPU cracking. A common starting point is 19 MiB of memory, 2 iterations, parallelism 1 — then tune upward to your latency budget.
PBKDF2 — old, weak by comparison, but FIPS-certified, which is why it persists in regulated environments. If you must use it, use a very high iteration count (600,000+ for PBKDF2-HMAC-SHA256) and understand you are accepting a worse option for compliance reasons.
Our bcrypt generator produces real bcrypt hashes at a chosen cost so you can see the format and time the work on your own hardware. It runs server-side, because bcrypt has no browser implementation — a fact stated on the page, since it is the one place on this site where the data does travel.
What good storage looks like
A modern password hash is self-describing. A bcrypt hash:
$2y$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyPFVGGCLKzOZi
encodes the algorithm (2y), the cost (12), the salt and the hash in one string. You store that single field. There is no separate salt column and no configuration to keep in sync — everything needed to verify is in the value, which is also what makes it possible to raise the cost factor gradually as users log in.
Things that seem clever and are not
Hashing twice. sha256(sha256(password)) doubles the attacker's work — from four billion guesses per second to two billion. Meaningless.
A secret salt ("pepper") instead of a proper function. A pepper stored outside the database is a genuine defence-in-depth measure, and it is an addition to correct hashing, never a replacement. Fast hash plus pepper loses the moment the application server is breached.
Your own construction. Combining functions in a novel way almost always weakens rather than strengthens, and there is no way to know from the outside.
Limiting password length or character set. A maximum of 16 characters, or a ban on symbols, usually signals that something downstream is not hashing at all. Hashing produces fixed-length output from any input; length limits exist only to prevent denial of service, and 128 characters is a generous cap.
The rest of the system
Hashing protects the database after a breach. It does nothing about the more common attacks.
- Rate limiting. An attacker guessing through your login form is limited by your form, not by your hash function. Throttle by account and by IP.
- Credential stuffing. Most account takeovers use passwords that were correct — on a different site. Checking new passwords against known breach corpora, and offering multi-factor authentication, addresses this; hashing cannot.
- Constant-time comparison. Verification functions handle this; do not hand-roll the comparison.
- Phishing. No password storage decision helps at all when the user types it into your attacker's page.
Strong hashing is table stakes. It buys you time and dignity after a breach. What prevents most real account compromises is everything in that list, plus users having passwords they did not reuse — which is what our password generator and strength checker are for, and why length beats complexity every time.
Tools mentioned in this guide
Bcrypt Generator
Generate a bcrypt hash at any cost factor and verify a password against an existing one, using PHP's own password_hash and password_verify.
Password Generator
Generate strong random passwords in your browser, with the length, character types and entropy you choose — and nothing sent to a server.
Password Strength Checker
Test how strong a password really is: entropy in bits, the weak patterns found inside it, and how long each named attack would take.
SHA256 Generator
Generate a SHA-256 hash of text or a file in your browser — the current standard for checksums, signatures and integrity checks.
More guides
- Why your link looks wrong when you share it Without Open Graph tags, every platform guesses what your page is about — and they guess badly, cache the guess, and give you no obvious way to correct it.
- Base64: what it is for, and when it quietly costs you It exists to move binary data through channels that only accept text. It is not compression, it is not security, and it makes everything about a third bigger.
- How to shrink a video enough to actually send it Every service has a different ceiling — 25 MB, 16 MB, 100 MB — and hitting it is not guesswork. Work backwards from the limit and the settings choose themselves.