When you register on a well-built website, your password is neither stored nor encrypted: it's hashed with a slow, salted function. That sentence condenses twenty years of lessons learned through million-record breaches. This article walks the complete journey of a password inside the server —and why each stage exists— useful whether you build systems or want to know which ones deserve your trust.
Why the password is never stored
The question answering everything: what happens if the database gets stolen? With passwords stored as-is (plaintext), the attacker gains instant access to every account — and to accounts on other services where your users repeat passwords. Encrypted with a key: better, but the key lives near the data and an attacker with system access usually has it too.
The correct model inverts the logic: store something that cannot log in but can verify. A cryptographic hash does exactly that: deterministic (same input, same output), irreversible (no inverse operation) and avalanche-prone (one changed bit transforms all output).
The fast-hash problem and the salt solution
A historical detail proved very costly: SHA-256 is fast by design — millions of hashes per second on GPU. Perfect for integrity verification; disastrous for password protection: the attacker steals the database and tests whole dictionaries at industrial speed (rainbow tables: precomputed hashes of millions of common passwords).
First defense — salt: a random unique-per-user value mixed into the password before hashing:
hash = H(salt + password)
store: salt + hash ← salt isn't secret, travels alongside
With salt, two users sharing a password produce different hashes, and precomputed rainbow tables die: regenerating them would require per-salt individual computation. Mass precomputation is dead.
But direct attack remains: testing candidates against ONE user still runs at GPU speed. Enter the second defense.
Deliberately slow functions
Password derivation functions deliberately do the opposite of SHA-256: take their time. Not milliseconds — tens or hundreds of milliseconds per attempt. The difference is asymmetric: for you logging in once, 100 ms is imperceptible; for an attacker testing 10 billion candidates, multiplying unit cost by 100,000 turns weeks into millennia.
Three living standards:
bcrypt (1999): the veteran. Configurable cost factor (each +1 doubles time), built-in salt, decades of analysis. Still acceptable today at cost ≥10-12.
// Node.js with bcryptjs
const hash = await bcrypt.hash(password, 12); // ~250 ms
await bcrypt.compare(attempt, hash); // true/false
scrypt (2009): adds memory resistance on top of CPU — especially punishing GPUs and ASICs, which have abundant compute but limited memory.
Argon2id (2015, Password Hashing Competition winner): today's recommended standard. Parameterizable in time, memory AND parallelism. Current typical setup: 19 MiB memory, 2 iterations, parallelism 1 — tunable to server hardware.
// Node.js with native argon2
const hash = await argon2.hash(password, { type: argon2.argon2id });
Choice rule in 2026: Argon2id if your platform supports it; bcrypt perfectly defensible; scrypt fine. Never MD5, never SHA-1, never bare SHA-256 for passwords.
Pepper: the optional layer
A pepper is a global secret value (not per user) added to the process, stored OUTSIDE the database — environment variable, KMS, HSM:
hash = Argon2id(pepper + salt + password)
Its grace: if the attacker extracts only the database (leaked SQL dump, lost backup), hashes are unverifiable without the pepper that wasn't there. Honest limitation: full system compromise yields the pepper too. Defense in depth, not absolute shield.
Classic mistakes (all seen in production)
Hashing with MD5 "because it works". Skipping salt ("I hash already, enough"). Truncating or mis-converting results (base64 case changes breaking verification). Capping password length at 8 "for compatibility" — with Argon2 you can accept 128 for free. And the worst: inventing your own scheme. Homebrew cryptography always loses; use maintained libraries with secure defaults.
The surrounding flow matters too: login rate limiting (slow hashing stops offline attacks, not online ones — that's what attempt limits are for), generic error messages ("incorrect user or password", never reveal which failed), and resets via single-use tokens with short expiry.
Check hashes yourself
If you want to see the avalanche effect live or compute SHA-256 over texts to understand hashing outside the password context, our hash generator does it locally. To gauge password strength before it reaches the hash, the entropy guide explains bit measurement, and if you're designing the full registration flow, creating a strong password covers the user side.
FAQ
How often should I rehash? At login you can transparently re-hash when parameters rose (bcrypt detects stored cost). Mass algorithm migrations need password resets or transitional double-hashing.
Can slow hashing take down my server with many logins? It's a real cost: Argon2id consumes memory per attempt. Limit concurrent attempts and tune parameters to your hardware — better a safe 100 ms than a fragile 10 ms.
Can I recover a password from its hash? No, and that's the point. You can only reset (generate new). Any service able to email you "your" password stores plaintext or reversibly-encrypted: run.
Compute instant SHA-256 hashes with our online hash generator, free and right in your browser.