How to Store Passwords Securely
Storing passwords securely is one of the most consequential decisions you make as a developer. Get it wrong and a single database breach exposes every user's credentials; get it right and even a full database dump leaves attackers with little to work with. This guide covers how to store the passwords your application receives from users, and separately how to store the passwords you use day to day.
Never store passwords in plaintext (or reversible encryption)
The first rule is the most important: do not store the password itself. If your database column holds the literal characters a user typed, anyone who reads that column knows the password. Logs, backups, and replicas all multiply the exposure.
A common but flawed instinct is to encrypt passwords instead. Encryption is reversible by design, which means the decryption key exists somewhere in your system and becomes the single point of failure. If an attacker gets the database, they will often get the key too. Passwords should be hashed, not encrypted, because you never need to recover the original value, only verify it.
Hash with a slow, salted password hashing function
A password hash is a one-way transformation. To check a login, you hash the submitted password and compare it to the stored hash. The catch is that general-purpose hashes like MD5, SHA-1, and even SHA-256 are far too fast: an attacker with a stolen database can compute billions of guesses per second on commodity GPUs.
Use a function purpose-built for passwords. As of today the well-established choices are:
- Argon2id — the modern default; tunable for memory, time, and parallelism, and resistant to GPU and ASIC cracking.
- scrypt — memory-hard and battle-tested; a solid choice where Argon2 is unavailable.
- bcrypt — older but still widely trusted; note its 72-byte input limit, after which extra characters are silently ignored.
- PBKDF2 — acceptable when a FIPS-validated algorithm is required, but it is not memory-hard, so use a high iteration count.
These functions are deliberately slow and accept a work factor (cost parameter) you raise over time as hardware improves. The goal is a hash that takes a fraction of a second for a legitimate login but is ruinously expensive to brute-force at scale.
Always use a unique salt per password
A salt is a random value, unique to each password, that you mix in before hashing. Salting defeats precomputed lookup tables (rainbow tables) and ensures two users with the same password get different hashes. You do not need to keep the salt secret — store it alongside the hash. The modern Argon2, bcrypt, and scrypt libraries generate a random salt automatically and embed it in the output string, so you typically store one self-describing field that contains the algorithm, parameters, salt, and hash together.
Consider a pepper for defense in depth
A pepper is a single secret value added to every password before hashing, but unlike a salt it is not stored in the database. Keep it in a secrets manager, environment variable, or HSM. If only the database leaks, the pepper remains unknown and the hashes are much harder to attack. A pepper complements salting; it does not replace it.
Implement verification and upgrades correctly
Use your library's built-in verify function rather than re-hashing and comparing strings yourself. A proper verify routine parses the stored parameters, re-derives the hash, and performs a constant-time comparison to avoid timing leaks.
- On signup, hash the password with current parameters and store the resulting string.
- On login, call the verify function with the submitted password and the stored hash.
- If verification succeeds and the stored work factor is below your current target, transparently re-hash with the new parameters and save it. This lets you strengthen security over time without forcing password resets.
You can experiment with a PBKDF2-based password hasher and inspect how salt and iterations affect the output using the Password Hasher (PBKDF2) tool, and explore general hashing behavior with the Hash Generator.
Common mistakes that undermine secure storage
- Using fast hashes. A single round of SHA-256 is not password storage, even with a salt.
- Reusing a global salt. One shared salt across all users reintroduces the rainbow-table problem.
- Logging credentials. Passwords leaking into request logs, stack traces, or analytics is a frequent breach vector.
- Capping or stripping characters. Avoid low maximum length limits and do not silently truncate; allow long passphrases and the full Unicode range.
- Rolling your own crypto. Use vetted libraries; subtle bugs in hand-written hashing are easy to introduce and hard to spot.
- Skipping rate limiting. Slow hashing protects stolen databases, but you still need login throttling and lockouts to stop online guessing.
Set sensible password policies
Current best practice favors length over arbitrary complexity. Encourage long passphrases, set a generous maximum, and screen new passwords against lists of known-breached credentials rather than imposing forced periodic resets, which tend to push users toward predictable patterns. Composition rules such as "one uppercase, one symbol" add little real entropy and frustrate users.
If you want to reason about how guessable a candidate password is, the Password Entropy Calculator shows how length and character set drive the search space, and the Password Strength Meter gives a quick practical estimate.
Storing your own passwords safely
Hashing protects passwords your application stores, but you also need a strategy for the passwords you personally use. The single most effective habit is a reputable password manager, which stores credentials in an encrypted vault unlocked by one strong master password (or passkey).
- Use a long, unique password for every site so one breach cannot cascade.
- Generate random credentials instead of inventing them; a Password Generator produces high-entropy strings on demand.
- Enable multi-factor authentication wherever offered, preferring an authenticator app or hardware key over SMS.
- Never store passwords in plaintext files, spreadsheets, browser notes, or source code.
Wherever the industry is moving toward passkeys and WebAuthn, adopt them: they replace shared secrets with public-key cryptography, so there is no reusable password to phish or leak in the first place. For more depth on user-facing guidance, see our companion guide on password security best practices.
A quick checklist
Before shipping authentication, confirm you can answer yes to each of these: passwords are hashed with Argon2id, scrypt, bcrypt, or PBKDF2; every hash has a unique random salt; the work factor is current and upgradeable; verification uses a constant-time comparison; credentials never appear in logs; and login attempts are rate-limited. Tick all six and you are well ahead of most production systems.
Frequently Asked Questions
Hash them. Encryption is reversible and requires a key that becomes a single point of failure if your database is breached. You never need to recover the original password, only verify it, so a one-way password hashing function like Argon2id, bcrypt, or scrypt is the correct tool.
No. SHA-256 is a general-purpose hash designed to be fast, so attackers can compute billions of guesses per second against a stolen database. Use a deliberately slow, salted password hashing function such as Argon2id, scrypt, bcrypt, or high-iteration PBKDF2 instead.
A salt is a unique random value per password that is stored alongside the hash; it defeats rainbow tables and makes identical passwords hash differently. A pepper is a single secret added to every password and kept outside the database in a secrets manager, so the hashes stay protected even if only the database leaks.
No. A salt's job is to be unique per user, not secret, and modern Argon2, bcrypt, and scrypt libraries embed it directly in the stored hash string. If you want an additional secret value, use a pepper, which is kept separately from the database.
Favor length over forced complexity: allow long passphrases and set a generous maximum, since arbitrary composition rules add little real entropy. Note that bcrypt only uses the first 72 bytes of input, so for very long passwords consider Argon2id or pre-hashing, and screen new passwords against known-breached lists.