A login system needs to recognize a password later, but it does not need to recover the original password. That distinction should shape how passwords are stored.

If an application stores plaintext passwords, reversible encrypted passwords, or fast general-purpose hashes, a database disclosure can turn into a much larger authentication problem. An attacker who obtains password verifiers can make guesses on their own hardware, without sending each attempt through the application’s login endpoint. Online rate limits no longer control that work.

Password hashing is designed for this situation. The goal is not to make guessing mathematically impossible. It is to make every offline guess expensive enough that a stolen verifier database is less useful, while keeping legitimate login verification practical.

This article builds that model from the smallest useful example, then explains salts, work factors, memory cost, parameter upgrades, and the limits of password hashing as a defensive control.

Design for the database-disclosure threat

Start with the threat model.

Assume an attacker obtains the application’s stored password-verifier records, perhaps from a database snapshot or backup. Assume the attacker can read the algorithm name, salt, parameters, and resulting hash. Those values do not need to be secret for password hashing to work as intended.

The attacker can now test candidate passwords locally:

candidate password + stored salt + stored parameters
                    |
             password hash
                    |
          compare with verifier

If the result matches, the attacker has found a password that satisfies that verifier. No request has to reach the real application during this process.

That is why login throttling and password hashing solve different problems. Throttling reduces the rate of guesses sent to your service. Password hashing raises the cost of guesses after verifiers have escaped your service.

Password hashing does not protect a password that is captured before hashing, such as by malware in the client, compromised application code, or a phishing site. It also cannot make a weak password strong. It changes the economics of offline guessing under a specific compromise assumption.

Store a verifier, not a recoverable password

A password-verification system normally needs one operation: determine whether a submitted password matches the password enrolled for the account.

It does not normally need this operation:

stored value -> recover user's original password

That makes reversible encryption the wrong default abstraction for password storage. If the application can decrypt every stored password using a key, compromise of that key and the encrypted database can expose the original passwords directly.

A password hash instead derives a verifier from the password. At authentication time, the application runs the submitted password through the same password-hashing scheme and compares the result with the stored verifier.

Use a password-hashing function designed for password storage, not a fast digest such as SHA-256. Fast hashes are useful for many integrity tasks precisely because they are cheap to compute. That property works against defenders when an attacker needs to evaluate millions of password guesses.

For a new system, use a well-maintained password-hashing library and a modern password-hashing scheme supported by your environment. Argon2id is a strong general choice where it is available. scrypt is another memory-hard option. PBKDF2 remains relevant in environments with particular platform or validation requirements. Legacy applications may already use bcrypt, whose input-length behavior and migration constraints need careful handling.

The important architectural decision is to use a dedicated, tunable password-hashing construction rather than inventing a combination of ordinary hash functions.

Give every password a unique salt

A salt is a value combined with a password as an input to password hashing. Each stored password should have its own salt chosen so that collisions are unlikely.

The salt is not a password and does not need to be hidden. Store it with the verifier when the library’s encoded output does not already contain it.

Consider two users who independently choose the same password. Without per-password salts, the same deterministic hashing process can produce the same stored value for both accounts. That leaks the fact that their password inputs match and lets an attacker reuse work across matching hashes.

With different salts, the inputs differ:

same password + salt A -> verifier A
same password + salt B -> verifier B

An attacker testing a candidate against verifier A must use salt A. Work performed for that salt does not directly provide the verifier for salt B.

Salts also undermine precomputed tables that assume a fixed unsalted transformation. They do not stop guessing. Because the salt is available to the attacker, the attacker can include it in every trial. The password-hashing function still needs to make those trials costly.

Modern password-hashing libraries often generate salts and encode them into the stored verifier automatically. Prefer that facility to manually assembling a storage format unless your platform requires otherwise.

Make each guess deliberately expensive

Password-hashing schemes expose cost parameters so defenders can tune how expensive verification is.

For an iteration-based construction, increasing the work factor increases computation. Memory-hard constructions such as Argon2id and scrypt also let the computation require substantial memory. This matters because an offline attacker wants to run many guesses in parallel; requiring meaningful computation and memory per guess raises that cost.

The same expense applies to your legitimate authentication service. That creates the central operational trade-off:

cost too low  -> offline guesses are unnecessarily cheap
cost too high -> login capacity and availability suffer

Do not copy a cost value indefinitely from an old example. Hardware, library implementations, traffic patterns, and service capacity change.

Start from current guidance for the selected algorithm, then benchmark the exact library and production-like hardware you intend to use. Measure authentication latency and resource consumption under realistic concurrency, not just a single hash on an idle development laptop. Leave capacity for traffic bursts and remember that attackers can deliberately trigger password verification too.

The right parameter set is therefore both a security decision and a capacity decision.

Keep the parameters with the verifier

A stored password record should carry enough information for the verifier to know how it was created. Depending on the library and format, one encoded string may already contain the algorithm identifier, cost parameters, salt, and derived value.

Conceptually, the record needs information like this:

algorithm:   Argon2id
parameters:  <chosen memory/time/parallelism settings>
salt:        <per-password salt>
verifier:    <derived output>

This is a teaching representation, not a recommendation to invent your own serialization format. Use the library’s standard encoded form when possible.

Keeping algorithm and parameter information with each verifier enables gradual migration. An application can recognize an older verifier, validate the submitted password using its old parameters, and then create a new verifier with the current policy after successful authentication.

That avoids requiring every active user to reset a password merely because the preferred work factor changed.

Upgrade cost when the user authenticates

Password-hashing parameters age. A cost that was expensive several years ago may become cheap relative to current hardware.

A practical migration pattern is:

verify submitted password with stored parameters
                  |
             match?
             /    \
           no      yes
           |        |
        reject   parameters old?
                    /       \
                  no         yes
                  |           |
               continue   hash password again
                          with current policy

The application can perform the rehash because the user has just supplied the plaintext password during legitimate authentication. The old stored hash alone is not enough to reconstruct that password.

This migration has an important boundary condition: accounts that never authenticate will not be upgraded automatically. For a system with long-dormant accounts, decide separately how long weak legacy verifiers may remain and whether some users should be required to reset their passwords before returning.

Test mixed-version records before rollout. A migration path is only useful if old verifiers remain verifiable until replacement and new records are written in the expected format.

Understand what a pepper changes

Some systems add a pepper as defense in depth. Unlike a salt, a pepper is secret and stored separately from the password-verifier database, such as in an appropriate secrets-management or key-management system.

The useful threat assumption is narrower than it first appears. If an attacker steals only the password database but not the pepper, the extra secret can make offline verification harder. If the attacker also compromises the application environment that can use the pepper, that advantage may disappear.

A pepper therefore does not replace salts, a suitable password-hashing function, or adequate cost parameters. It also adds recovery complexity. If a pepper is lost, password verification may become impossible. If it is exposed, changing it generally requires a migration strategy that accounts for the fact that the server does not know users’ plaintext passwords while they are offline.

Use a pepper when the additional trust separation is meaningful for your architecture and you can operate its lifecycle correctly. Do not add one merely to compensate for weak password hashing.

Avoid constructions that look stronger but are not

Several designs add complexity without solving the offline-guessing problem well.

Storing SHA-256(password) is inadequate because SHA-256 is intentionally fast. Adding a salt to a fast hash fixes salt-related problems but still leaves each guess cheap. Repeating several different fast hashes by hand creates a custom construction whose security and migration behavior you now have to reason about yourself.

Encrypting passwords preserves recoverability, which is normally unnecessary for authentication and creates a high-value decryption key. Encoding passwords with Base64 or another reversible representation provides no cryptographic protection.

Another common mistake is treating the salt as a secret. A design should remain defensible when the attacker knows the salt and hashing parameters. If secrecy outside the database is part of the threat model, that is the separate role a pepper can play.

Verify the control as an operational property

Do not stop at confirming that a library function appears in the source code. Test the properties that the design depends on.

Create two accounts with the same password and confirm their stored verifier records differ because they use different salts. Confirm a valid password verifies and a different password does not. Inspect newly created records to ensure they use the intended algorithm and current parameters.

Then create a test record with an older supported parameter set. Authenticate successfully and confirm that the record is upgraded to the current policy. Also test failure paths: a rejected password must not trigger a verifier rewrite, and malformed stored records should fail predictably rather than silently falling back to a weaker scheme.

Finally, load-test the verification path. A password-hashing configuration that is strong in isolation but exhausts authentication workers under ordinary concurrency can create an availability problem.

Combine password hashing with controls for other threats

Password hashing protects stored password verifiers under an offline-attack threat model. It does not replace controls on the rest of the authentication system.

Use transport protection so passwords are not exposed in transit. Protect application hosts and secrets so attackers cannot capture passwords before hashing. Rate-limit online authentication attempts. Offer stronger authentication methods where the account risk justifies them. Keep password-reset and account-recovery flows at least as carefully protected as normal login.

These controls are complementary because they operate at different points in the attack path.

The practical rule is simple: store passwords as salted, deliberately expensive password verifiers using a maintained password-hashing scheme, retain the algorithm and parameters needed to verify each record, and plan for those parameters to change. The purpose is not to make a database compromise harmless. It is to ensure that stealing the verifier database does not make large-scale password guessing unnecessarily cheap.