Blog › Cryptography

Password Salting Explained: Why Every Password Needs a Unique Salt

By Thomas Hart, Security EducatorJuly 13, 20268 min read

Password salting is one of the most important concepts in password security — and one that operates entirely behind the scenes, handled by servers, not users. Understanding it helps you understand why some breaches are catastrophic and others aren't, and what it means when a company says "passwords were hashed."

Why plain hashing isn't enough

When websites store passwords as hashes (e.g., SHA-256("password") = 5e884898...), two problems emerge:

  1. Duplicate passwords produce duplicate hashes — If two users both use "password123," their stored hashes are identical. Crack one, crack both. An attacker can see at a glance how many users share each password.
  2. Precomputed attacks work — Attackers can build a massive lookup table (hash → password) offline and instantly reverse any hash in it. These are called rainbow tables.

A salt fixes both problems simultaneously.

What is a salt?

A salt is a randomly generated string that's added to a password before hashing. Each user gets their own unique salt, generated fresh at account creation.

Without salting — identical passwords produce identical hashes:

User A: hash("password123") = 482c811da5d5b4bc6d497ffa98491e38

User B: hash("password123") = 482c811da5d5b4bc6d497ffa98491e38

With unique salts — same passwords produce completely different hashes:

User A: hash("password123" + "xK7qm2pL") = a1b2c3d4e5f6789...

User B: hash("password123" + "nR9sW4vY") = f9e8d7c6b5a4321...

How salts are stored

Salts are not secret — they're stored alongside the hash in the database. This might seem counterintuitive, but the security doesn't come from hiding the salt. It comes from making the hash unique per user and per password, which forces an attacker to run a fresh brute-force attack against each hash individually rather than using precomputed tables.

Modern password hashing functions embed the salt directly in the output string:

bcrypt hash (contains version, cost, salt, and hash):

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LeKQqE6VQlF.TtTx2

The highlighted portion is the salt, stored right in the hash.

Why salts need to be unique per user

If a site uses the same salt for all users (a "pepper" or static salt), it only partially addresses the problem. An attacker can still build a custom rainbow table for that specific salt, then crack every hash in the database at once.

With unique per-user salts, the attacker must brute-force each hash independently. There's no shortcut. A database of 10 million users with unique salts requires 10 million independent brute-force attacks, not one.

How salt length affects security

The salt needs to be long enough that two users are statistically guaranteed to get different salts. Consider:

  • A 32-bit salt: 4 billion possible values. With millions of users, collisions become likely (birthday paradox).
  • A 64-bit salt: ~18 quintillion values. Collisions extremely unlikely even with billions of users.
  • A 128-bit salt: effectively infinite. No collisions, ever. Standard for modern implementations.

bcrypt uses a 128-bit (16-byte) salt by default. Argon2 uses 128 bits. scrypt uses configurable salt sizes. All are adequate.

What salting means for users

Salting is entirely server-side — there's nothing you need to do as a user. But understanding it helps you evaluate breach disclosures:

✅ "Passwords were hashed with bcrypt"

Good. bcrypt includes salting and is slow by design. Your password is safe as long as it's reasonably strong.

⚠️ "Passwords were SHA-256 hashed"

Ambiguous. If unsalted: change your password everywhere you used it. If salted: still change it for the breached site, but exposure is more limited.

❌ "We store passwords securely" (vague)

If they won't tell you the algorithm, assume the worst. Change your password and use a unique password for that site going forward.

❌ You received an email with your actual password

The site is storing passwords in plaintext. Change your password everywhere you used it, report the practice, and consider using a different service.

What about peppers?

A pepper is a server-side secret value added to passwords before hashing, separate from the salt. Unlike salts, peppers are stored in application configuration (not the database), so if the database is stolen but the application server isn't, the pepper isn't exposed.

Peppers add a layer of protection against database-only breaches, but they're not a replacement for proper per-user salting. They're an optional additional layer used by some high-security systems.

Frequently asked questions

Do I need to salt passwords manually?

No. Modern password hashing libraries (bcrypt, Argon2) handle salting automatically. Never implement your own salting — use battle-tested libraries. If you're using a web framework, it almost certainly has password hashing built in (e.g., Rails has has_secure_password, Laravel has Hash::make(), Django uses PBKDF2 by default with salts).

Can salts be predictable (like the username or user ID)?

No. Salts must be cryptographically random. Using predictable values (username, email, user ID, timestamp) undermines the security because an attacker can precompute rainbow tables specifically for those predictable salts. Always use a cryptographically secure random number generator to produce salts.

What's the difference between a salt and a pepper?

A salt is a unique, per-user random value stored in the database alongside the hash. A pepper is a single application-wide secret stored in server configuration, not the database. Salts protect against precomputed attacks and expose each hash independently. Peppers add protection if the database is stolen without the application server.