When a website stores your password properly, it never stores the password at all. It stores a hash - A one-way fingerprint. But a bare hash has a fatal weakness: the same password always produces the same fingerprint. Every user who picked "sunshine1" gets an identical hash, and attackers arrive with billions of fingerprints precomputed. The fix costs sixteen random bytes and has been standard practice since Unix in the 1970s. It's called a salt. Its lesser-known cousin, the pepper, adds a second layer that most people have never heard of. Together they decide whether a stolen password database cracks in hours or holds for years.
The problem with plain hashes
Hash functions like SHA-256 are deterministic by design: the same input always yields the same output. You can watch this yourself with our SHA-256 generator - Type "password123" today, tomorrow, or on any computer on Earth and you'll get the same 64-character digest every time.
Determinism is exactly what attackers exploit. If a database of unsalted hashes leaks, three shortcuts open up immediately:
- Lookup tables. Attackers have long since hashed every common password and every leaked password in history. Cracking becomes a database join, not a computation.
- Rainbow tables. A space-optimized version of the same idea: enormous precomputed chain structures that trade a little CPU time for vastly smaller storage.
- Batch cracking. One guess can be checked against every user at once. Hash "qwerty2024" a single time and compare it to a million rows simultaneously.
The LinkedIn breach of 2012 is the canonical cautionary tale: roughly 6.5 million unsalted SHA-1 hashes appeared online (later revealed to be closer to 117 million accounts), and the bulk of them were cracked within days precisely because no salt was used.
What a salt actually is
A salt is a random value - Typically 16 bytes from a cryptographically secure random generator - Created fresh for each user and combined with the password before hashing:
stored_value = Hash(salt + password)
The salt is saved in plain sight, right next to the hash in the same database row. That surprises people, but it's fine, because a salt's job is not secrecy. Its job is uniqueness. With salts in place:
- Two users with the same password now have completely different hashes.
- Precomputed lookup and rainbow tables become worthless - Nobody precomputed your specific 16-byte salt.
- Every guess must be recomputed per user. Attacking a million accounts costs a million times the work of attacking one.
Modern password hashing algorithms handle this automatically. When you see a bcrypt string like $2b$12$N9qo8uLOickgx2ZMRZoMye..., the salt is embedded right inside it, along with the cost setting. Developers using Argon2id, bcrypt, or scrypt never generate salts manually - The library does it correctly by default.
Rule of thumb: a salt makes an attacker crack one user at a time; a pepper makes them breach two systems instead of one; a slow hash makes every single guess expensive. You want all three.
What a pepper adds
A pepper is different in one crucial way: it is secret. It's a single high-entropy key (or per-application value) that lives outside the password database - In an environment variable, a secrets manager, or ideally a hardware security module (HSM). A common construction is to compute an HMAC of the password with the pepper first, then feed the result to a slow hash:
stored_value = Argon2id(salt, HMAC-SHA256(pepper, password))
Now consider the most common breach scenario: an SQL injection or a leaked database backup exposes the password table, but not the application servers. Without the pepper, the attacker literally cannot begin cracking - Every candidate guess produces the wrong input to the hash. NIST SP 800-63B endorses exactly this approach, recommending an additional keyed hash with a key stored separately from the hashed passwords, such as in an HSM.
The trade-offs are operational rather than cryptographic. If you ever lose the pepper, every stored hash becomes unverifiable and all users must reset their passwords. Rotating a pepper is awkward because you can't re-compute old entries without knowing each user's password. That's why peppers are standard at large providers with mature key-management, and rarer at small sites.
Salt vs pepper at a glance
| Property | Salt | Pepper |
|---|---|---|
| Secret? | No - Stored beside the hash | Yes - Kept out of the database |
| Unique per user? | Yes, always | No - Typically one per application |
| Typical size | 16 random bytes | 32 random bytes |
| Defends against | Rainbow tables, batch cracking | Cracking a stolen database dump alone |
| Stored in | The password table itself | Env variable, secrets manager, or HSM |
| If lost or leaked | No direct harm - It was never secret | Leaked: back to salt-only; lost: forced resets |
What salts and peppers don't fix
Here's the part that gets skipped in most explanations: salting a fast hash is still a weak scheme. A salted SHA-256 hash defeats precomputation, but a single modern GPU rig can still test billions of salted SHA-256 candidates per second against one user. Salts force per-user work; they don't make that work slow.
That's the job of the third ingredient - A deliberately expensive, memory-hard hashing algorithm. Our guide to how attackers crack stolen hashes walks through the GPU economics, and the short version is stark: the difference between salted SHA-256 and a properly tuned Argon2id is a factor of tens of thousands in guesses per second.
And no server-side scheme rescues a genuinely weak password. If your password is in the top million most common choices, an attacker cracks it under any algorithm, salted or not - It simply appears too early in the guess order. Salting protects the strong passwords; it buys the weak ones only minutes. The entropy math behind crack times explains why length and randomness dominate everything the server does. A 16-character random string from a password generator stays uncrackable even under a fast, badly configured hash.
How this plays out in real breaches
Breach post-mortems read like a controlled experiment in seasoning:
- RockYou (2009): 32 million passwords stored in plaintext - No hash, no salt, nothing. The leak became the standard cracking wordlist still used today.
- LinkedIn (2012): unsalted SHA-1. The majority cracked within days.
- Ashley Madison (2015): bcrypt with cost 12 - Genuinely strong - But a programming error left a parallel table of fast MD5 hashes, and roughly 11 million passwords fell through that side door. You can see how trivially fast MD5 computes with our MD5 hash tool.
- Dropbox (2016 disclosure): bcrypt with per-user salts plus a pepper. Despite 68 million records leaking, no mass cracking of the bcrypt portion ever materialized publicly.
The pattern is consistent: the algorithm and its seasoning, chosen years before any breach, determine almost everything about the damage afterward.
What this means for you
As a user, you get no menu of hashing options - But your choices still dominate the outcome. Use a unique random password per site so that one cracked database never cascades into others. Prefer length over cleverness. And when a breach notification arrives, change that password immediately, because you have no idea whether the site salted, peppered, or stored plaintext until the post-mortem - Which often takes months.
As a developer, the checklist is short: use Argon2id (or bcrypt/scrypt) via a maintained library, let it generate the salt, add a pepper via HMAC if you have real key management, and never roll your own construction.
FAQ
Is a salt supposed to be kept secret?
No. A salt's value comes entirely from being unique per user, not from being hidden. It's stored in the same row as the hash and is assumed to be visible to any attacker who steals the database. If your security depends on the salt staying secret, what you actually have is a poorly implemented pepper.
How long should a salt be?
16 bytes (128 bits) from a cryptographically secure random generator is the standard, and it's what libraries for Argon2id and bcrypt produce by default. NIST SP 800-63B sets the floor lower, but there's no reason to go below 16 bytes - Salts are free. What matters most is that each one is random and never reused across users.
Can two users end up with the same salt?
With 128-bit random salts, the odds are negligible - You'd expect a collision only after generating around 2⁶⁴ salts, far more users than any service has. Collisions were a real problem historically when systems used tiny salts: classic Unix crypt used just 12 bits, allowing only 4,096 possible salts, so large systems inevitably repeated them.
Does a pepper make a weak password safe?
Only against offline cracking of a database-only leak. If attackers compromise the application server too, they get the pepper and cracking proceeds normally - Weak passwords fall first. And a pepper does nothing against online guessing, credential stuffing, or phishing. It's one layer, not a substitute for a strong password.
Why not just encrypt passwords instead of hashing them?
Encryption is reversible by design - Anyone who obtains the key can decrypt every password at once, and the site itself can read your password, which you never want. Hashing is one-way: even the site can only verify a password, not recover it. The right design is a salted, peppered, deliberately slow hash - Which is exactly what Argon2id delivers.