AlgoInsight

System Design

How to Securely Store Passwords in a Database

Storing passwords wrong is one of the most damaging mistakes a system can make - and it's still shockingly common. The right way is well-established and not hard. Here's exactly how to store passwords so that even a database breach doesn't expose them.

By Admin2026-06-1815 min readsecuritypasswordshashingdatabasessystem-designauthentication

Of all the security mistakes a system can make, storing passwords badly is among the most damaging - and, distressingly, among the most common. When a database gets breached (and breaches happen), how you stored the passwords determines whether attackers walk away with every user's actual password or with useless gibberish. The difference is entirely in the approach, and the right approach is well-established, not difficult, and something every engineer should understand.

This guide lays out exactly how to store passwords securely, building from the wrong ways (so you understand why they're wrong) to the right way that protects users even when the database is breached.

The cardinal rule: never store the actual password

Start with the absolute rule: never store passwords in plaintext. If your database stores passwords as the actual text the user typed, then anyone who gets access to that database - an attacker, a malicious insider, anyone who finds a leaked backup - instantly has every user's password. And because people reuse passwords across sites, you've handed attackers the keys to your users' other accounts too. Plaintext password storage is a catastrophic failure, and it still happens.

The goal of secure password storage is this: store something that lets you verify a user's password when they log in, but that does NOT let anyone recover the actual password - even with full access to the database. That sounds paradoxical, but a tool makes it possible: hashing.

Why not encryption?

A tempting idea: encrypt the passwords. But encryption is the wrong tool here, because encryption is reversible - encrypted data can be decrypted back to the original with the key. That means the key exists somewhere, and if an attacker who breaches your database also gets the key (often stored nearby, or on the same compromised systems), they can decrypt every password back to plaintext. Reversibility is exactly what you don't want. You don't actually need to ever recover the original password - you only need to check whether a login attempt matches. So you want a one-way transformation, not a reversible one. That's hashing.

Hashing: the one-way transformation

A hash function takes input and produces a fixed-size output (the hash), in a way that's one-way - you can't reverse the hash back to the input. The idea for passwords:

Because the hash is one-way, an attacker who steals the hashes can't reverse them to get the passwords. You can verify logins without ever storing the actual password. This is the foundation - but plain hashing alone has two serious weaknesses that the rest of this guide closes.

sequenceDiagram
    participant U as User
    participant A as Application
    participant DB as Database
    Note over U,DB: Registration
    U->>A: Sets password
    A->>A: Generate salt + hash(password + salt)
    A->>DB: Store hash + salt (never the password)
    Note over U,DB: Login
    U->>A: Enters password
    A->>DB: Fetch stored hash + salt
    A->>A: hash(entered password + salt)
    A->>A: Compare to stored hash
    A-->>U: Match → logged in / No match → rejected
This sequence diagram shows secure password handling. During registration, the user sets a password; the application generates a random salt, hashes the password together with the salt, and stores the hash and the salt in the database - never the password itself. During login, the user enters their password; the application fetches the stored hash and salt, hashes the entered password with that same salt, and compares the result to the stored hash. A match logs the user in; no match is rejected. The actual password is never stored.

Weakness 1: fast hashes are brute-forceable - use a slow, dedicated function

The first problem: general-purpose hash functions (like the ones used for checksums and integrity) are designed to be fast. That speed is a virtue for most uses but a serious flaw for passwords, because it lets an attacker who steals the hashes try billions of candidate passwords per second, hashing each and checking for a match. Fast hashing makes brute-forcing and guessing common passwords feasible.

The fix: use a slow, purpose-built password hashing function designed specifically to be computationally expensive, so that each guess takes meaningful time and brute-forcing becomes impractical. The established choices:

These functions are intentionally slow and have a tunable work factor (or cost parameter) - you can dial up how expensive they are. The point: a function that takes a fraction of a second per hash is no problem for a legitimate login (one hash) but devastating for an attacker trying billions of guesses. Crucially, this work factor can be increased over time as hardware gets faster, keeping password storage hard to crack for years. Never use a plain fast hash for passwords; always use one of these dedicated slow functions.

Weakness 2: identical passwords hash identically - add a salt

The second problem: if you hash passwords the same way every time, two users with the same password get the same hash. Attackers exploit this with precomputed tables (rainbow tables) mapping common passwords to their hashes - steal your hashes, look them up, done. Identical hashes also reveal which users share a password.

The fix: a salt - a unique, random value generated for each password and combined with it before hashing. Because every password gets a different salt, identical passwords produce different hashes, and precomputed tables become useless (the attacker would need a separate table for every salt). You store the salt alongside the hash (the salt isn't secret - its job is to be unique, not hidden), and at login you use the stored salt to hash the entered password the same way.

Salting is essential and non-negotiable: it defeats rainbow tables and ensures that even users with identical passwords have unrelated hashes. Modern password hashing functions like Argon2 and bcrypt handle salting for you, generating and incorporating a unique salt automatically - another reason to use them rather than rolling your own.

Putting it together: the correct approach

The complete, established best practice for storing passwords:

  1. Never store the plaintext password. Ever.
  2. Use a slow, dedicated password hashing function - Argon2 (preferred), bcrypt, scrypt, or PBKDF2 - not a fast general-purpose hash, and not encryption.
  3. Use a unique random salt per password (the dedicated functions do this for you), stored alongside the hash.
  4. Set an appropriate work factor, and increase it over time as hardware improves.
  5. At login, hash the entered password with the stored salt and the same function, and compare to the stored hash.
# Conceptual illustration using a dedicated password-hashing library.
# (Use a well-maintained library; never implement password hashing yourself.)

# Registration - store the hash, never the password
hashed = password_hasher.hash(user_password)   # generates a unique salt + slow hash internally
store_in_database(username, hashed)             # store 'hashed' (includes the salt)

# Login - verify without ever recovering the password
stored = fetch_from_database(username)
if password_hasher.verify(stored, entered_password):
    grant_access()
else:
    reject()

Done this way, a database breach yields only salted, slow hashes - computationally impractical to reverse - so users' actual passwords stay protected even though the database is in the attacker's hands. That's the whole goal achieved.

Understanding the attacks you're defending against

The best practices make more sense when you understand the attacks they defend against - because each rule closes a specific attack.

The offline brute-force attack. When an attacker steals your database of password hashes, they take them offline and try to crack them at their leisure - generating candidate passwords, hashing each, and checking for matches, as fast as their hardware allows. With a fast hash, they can try billions of candidates per second, cracking weak and common passwords quickly. This is the attack a slow hashing function defends against: if each hash attempt takes meaningful time, billions of guesses become impractical. Slow hashing turns a feasible attack into an infeasible one.

The dictionary attack. Rather than trying random combinations, attackers try lists of common passwords and previously-breached passwords first, because people pick predictable passwords. This is why work factor (slowing each guess) and encouraging strong passwords both matter - you slow the guessing and reduce how many users have guessable passwords.

The rainbow table attack. A rainbow table is a precomputed mapping from common passwords to their hashes. If you hash without salts, an attacker just looks up your stolen hashes in the table - no guessing needed, instant cracks for any password in the table. This is precisely what salting defeats: with a unique random salt per password, the attacker's precomputed table is worthless, because your hashes were computed with salts the table doesn't account for. They'd have to build a fresh table for every single salt, which defeats the entire point of precomputation.

So the defenses aren't arbitrary: slow hashing defeats offline brute-forcing, salting defeats rainbow tables, and strong-password practices plus work factor defeat dictionary attacks. Each rule closes a specific attack.

What about a "pepper"?

Beyond salting, some systems add a pepper - a secret value combined with the password before hashing, like a salt but secret and not stored in the database (kept separately, for instance in application configuration or a secrets manager). The idea: even if the database (with its salts and hashes) is breached, an attacker still lacks the pepper, adding another layer they must overcome.

A pepper is an optional extra layer, not a replacement for the essentials. Salting and slow hashing are mandatory; a pepper is a defense-in-depth addition that helps only if the secret is genuinely kept separate from the database (if it's stored in the same place that gets breached, it adds nothing). Know it exists as an option, but don't let it distract from the non-negotiables: slow function, unique salt per password.

Upgrading security over time

Password storage isn't set-and-forget, because hardware keeps getting faster and standards evolve. Two practices keep it current:

These practices reflect that secure password storage is an ongoing commitment to staying ahead of attackers' growing capabilities - not a one-time setup. A scheme that was strong five years ago can be weak today, so revisiting work factors and hashing choices periodically is part of doing this right.

Beyond storage: defense in depth

Secure storage is the foundation, but a few complementary practices strengthen authentication overall:

These layer on top of secure storage to make the whole authentication system robust.

Important: The single most important takeaway: store passwords as salted hashes using a slow, dedicated password-hashing function (Argon2, bcrypt, scrypt, or PBKDF2) - never as plaintext, never with reversible encryption, and never with a fast general-purpose hash. Done right, a full database breach still doesn't expose your users' passwords. Done wrong, a single breach hands attackers every password. The right way is well-established and built into standard libraries - there's no excuse to get it wrong.

Interview: Password storage is a common system design and security question, and a clear answer signals real security awareness. The expected answer: never store plaintext or use reversible encryption; hash with a slow, dedicated function like bcrypt or Argon2; use a unique salt per password to defeat rainbow tables; and tune the work factor to stay slow as hardware improves. Mentioning why (a breach should yield uncrackable hashes, fast hashes are brute-forceable, salts stop identical passwords from sharing a hash) shows you understand the reasoning, not just the recipe. Adding MFA and rate limiting as defense-in-depth rounds out a strong answer.

The bottom line

Storing passwords securely means storing something that verifies a login but can't be reversed to the password - even with full database access. The wrong ways: plaintext (catastrophic) and reversible encryption (the key leaks too). The right way: hash with a slow, purpose-built password hashing function (Argon2, bcrypt, scrypt, or PBKDF2) so brute-forcing is impractical, with a unique random salt per password so identical passwords don't share a hash and rainbow tables are useless, and a work factor you increase over time. Verify logins by hashing the entered password with the stored salt and comparing.

Done correctly - using established libraries, never rolling your own - a database breach yields only uncrackable hashes, and users' real passwords stay safe. Layer on rate limiting and MFA for defense in depth. The right approach is standard and well-supported; the only real mistake is not following it. We cover security fundamentals across System Design Fundamentals.

Note: This is general security education on established best practices. For production systems, follow current authoritative guidance (such as the OWASP Password Storage Cheat Sheet linked below) and use well-maintained, audited libraries rather than custom implementations.


Go deeper

More articles