What Is a Salt in Password Hashing — and Why Every Password Needs One
A salt is a random string mixed into a password before it's hashed. That's it. The reason salts exist is to make sure two users who happen to choose the same password end up with completely different hashes in the database.
Without salts, the password "password123" hashes to the same value for every user who chooses it — across every site on the internet that uses the same hash function. Attackers precompute that hash once and look for matches everywhere. With salts, that attack stops working entirely.
What a salt does
- Makes two users with the same password get different stored hashes.
- Defeats rainbow tables — precomputed lookups from hash → password.
- Forces an attacker to crack each password individually instead of all at once.
- Is stored in plaintext alongside the hash. It's not a secret.
- Is generated randomly per password, typically 128 bits.
Why same passwords are a problem
Suppose 100 users on your site all chose "summer2024". Without a salt, each of their database records contains the same hash:
alice e4b2c... (sha256 of summer2024)
bob e4b2c... (sha256 of summer2024)
charlie e4b2c... (sha256 of summer2024)
... 97 more rows with e4b2c...
An attacker who breaches the database doesn't need to crack each row. They just need to crack one. The moment they figure out that e4b2c... = summer2024, all 100 accounts fall instantly. Worse, if any other website also stores SHA-256 of summer2024 (and it does — that's an extremely common password), the attacker can match across breaches.
With a unique salt per user:
alice salt=a1b2... hash=8f3d... (sha256 of a1b2...summer2024)
bob salt=c9e4... hash=4a92... (sha256 of c9e4...summer2024)
charlie salt=7f31... hash=ba0c... (sha256 of 7f31...summer2024)
Now each row is unique. Cracking Alice's hash tells you nothing about Bob's. The attacker must brute-force each password independently — multiplying their work by the number of users.
Salts kill rainbow tables
A rainbow table is a giant precomputed mapping from hashes back to passwords. Decades of leaked passwords have been hashed with MD5, SHA-1, and SHA-256 and stored in public databases. If you find a hash in one of these tables, you instantly know the original password.
For a rainbow table to work, the attacker needs to know the hashing scheme in advance. Plain MD5? Trivial. Plain SHA-256? Trivial. But MD5 of "3f7a92c8 + password"? The attacker would need to build a new table for that specific salt. With a unique random salt per user, the attacker would need to build 100 tables for 100 users — at which point it's no faster than just brute-forcing each password individually.
This is why salts are required by every modern password storage standard, including OWASP, NIST SP 800-63B, and PCI DSS.
How big should a salt be?
The salt needs to be large enough that two different users almost certainly get different salts. The standard is 128 bits (16 bytes) of cryptographically random data. With 2¹²⁸ possible salts, the chance of two users on the same site getting the same salt is essentially zero even with billions of users.
You generate the salt with a cryptographically secure random number generator — crypto.randomBytes(16) in Node.js, secrets.token_bytes(16) in Python, SecureRandom in Java. Never Math.random(), never the current timestamp, never username-based "salts".
Where the salt is stored
The salt is stored in plaintext right next to the hash. This is fine and intentional. The salt isn't supposed to be secret. Its only job is to make sure no two stored hashes are the same.
In modern password hash formats, the salt is embedded directly in the hash output:
bcrypt: $2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
↑ ↑ ↑
cost salt (22 chars) hash (31 chars)
Argon2id: $argon2id$v=19$m=65536,t=3,p=1$c2FsdHNhbHQ$aGFzaGhhc2g
↑ ↑ ↑ ↑ ↑
variant ver params salt hash
Verification reads the salt out of the hash string, re-runs the algorithm with the candidate password and that salt, and compares results. You never need to manage salts manually if you're using bcrypt, Argon2id, or scrypt correctly.
Pepper: an extra layer (optional)
A pepper is like a salt, but it's secret and the same for every user. Where the salt goes in the database next to the hash, the pepper lives somewhere else entirely — typically in an environment variable, a hardware security module, or a separate secrets store that isn't backed up to the same place as the database.
The idea: if an attacker steals your database but doesn't get the pepper, they can't crack any password offline — the pepper acts as an unknown constant they have to guess in addition to the password itself.
Peppers are optional and somewhat controversial. They add operational complexity (now you have a secret to rotate) and they don't help if the attacker compromises both the database and the application server. For most applications, a good salt + a strong slow hash (bcrypt or Argon2id) is sufficient.
Common salt mistakes
- Hardcoded salts. Every user gets the same salt. This is basically not salting at all — it just means the attacker builds one rainbow table specific to your application.
- Username as salt. Predictable across systems. If user "
alice" exists on multiple sites, all her hashes can be attacked in parallel using a custom rainbow table for username "alice". - Short salts. A 32-bit salt (only 4 billion possibilities) is small enough that an attacker with enough storage could precompute rainbow tables for every salt value. 128 bits is the standard.
- Reused salt for password changes. When a user changes their password, generate a new salt too. Otherwise pattern analysis on old vs. new hash leaks information.
- Storing salt and hash in different tables that can desync. If a backup restores the salt table to a different state than the hash table, all your password verification breaks. Embed the salt in the hash string itself, as bcrypt and Argon2id do.
The good news: you usually don't have to think about this
If you're using bcrypt, Argon2id, scrypt, or PBKDF2 through their standard libraries, the salt is generated, stored, and used for verification automatically. You call bcrypt.hash(password, 12) and get back a complete $2b$12$... string with embedded salt. You call bcrypt.compare(password, hash) and verification just works. The library handles everything.
The mistakes happen when someone tries to be clever — reinventing salting on top of plain SHA-256, or building "their own" auth library, or storing salts in weird ways. Don't. Use the standard library. It's already correct.
Try generating a bcrypt hash with our bcrypt tool — every time you click Generate, you get a different hash for the same password because a fresh salt is auto-generated. That's what proper salting looks like in practice.