Password Hashing Best Practices (Why Not SHA-256)
By AZ Utils Editorial · · 12 min read
If you take one rule away from this entire cluster, make it this: never store passwords with a plain, fast hash like SHA-256 or MD5. Password storage is its own discipline with its own tools, and getting it wrong is one of the most damaging mistakes in software, because a breach exposes every user's password. This guide lays out password hashing best practices — what to use, why fast hashes fail, and how to do it correctly.
It is written for developers who build authentication, engineers reviewing security, and anyone who needs to store user passwords safely.
The Core Rule: Slow, Salted, Purpose-Built
Passwords must be stored using a slow, salted, adaptive password-hashing function designed specifically for the job. The leading choices are Argon2 (the modern winner of a public competition for password hashing), scrypt, bcrypt and PBKDF2. Each of these takes a password and produces a hash that is deliberately expensive to compute, individually salted, and tunable so it can be made slower as hardware improves. You never store the password itself, and you never use a general-purpose hash like SHA-256 or MD5 directly.
The reason these functions exist as a separate category is that the goals of password hashing are the opposite of the goals of a general hash. A general hash like SHA-256 is prized for being fast and is perfect for integrity checks. A password hash is prized for being slow and resource-intensive, because the only thing standing between a stolen database and the plaintext passwords is how long it takes an attacker to guess them. Slowness, which would be a defect in an integrity hash, is the entire point of a password hash.
In short: Store passwords with a slow, salted, adaptive hash — Argon2, scrypt, bcrypt or PBKDF2 — never with a fast hash like SHA-256 or MD5. The salt defeats precomputed tables, and the deliberate slowness defeats brute-force guessing.
Why Fast Hashes Fail for Passwords
To understand the rule, imagine an attacker who has stolen your user database, including the stored password hashes. Their goal is to recover the original passwords by guessing: they try a candidate password, hash it the same way you did, and check whether the result matches a stored hash. The faster your hash function, the more guesses they can try per second.
This is exactly why SHA-256 and MD5 are catastrophic for passwords. They are designed to be extremely fast, so modern hardware — especially GPUs and specialised machines — can compute billions of them per second. Against such speed, every common password, every dictionary word, and every short combination falls almost instantly. A fast hash turns a database breach into a near-total password compromise within hours. A purpose-built password hash, tuned to take a meaningful fraction of a second per guess, reduces the attacker's rate from billions per second to a few thousand, which transforms an afternoon's work into something computationally hopeless for strong passwords.
People sometimes try to patch a fast hash by running it many times in a loop, and indeed iterating is part of how PBKDF2 works. But rolling your own iteration scheme is error-prone, and the established functions already do this correctly with carefully chosen parameters and additional protections. Reaching for a proven password hash is both easier and safer than trying to slow down SHA-256 by hand.
Salting: Defeating Precomputation
The second pillar is the salt: a unique, random value generated for each password and stored alongside its hash. The salt is combined with the password before hashing, so two users who happen to choose the same password end up with completely different stored hashes.
Salting defeats a whole class of attacks based on precomputation. Without salts, an attacker can build (or download) enormous precomputed tables — including "rainbow tables" — that map common passwords to their hashes once, and then crack any unsalted database instantly by lookup. A unique salt per password makes such tables useless, because the attacker would need a separate table for every possible salt, which is infeasible. It also means cracking must be done separately for each user rather than once for the whole database. Crucially, the salt does not need to be secret — it is stored in plain alongside the hash — it only needs to be unique and random per password. The good news is that all the recommended password-hashing functions generate and manage salts for you, so you typically do not handle them manually at all.
The Work Factor: Tuning the Cost
The third pillar is the work factor (also called the cost or iteration count), a parameter that controls how expensive the hash is to compute. You set it so that hashing a password takes a deliberately noticeable amount of time — commonly in the region of a fraction of a second on your server. This is barely perceptible to a legitimate user logging in once, but it is devastating to an attacker trying billions of guesses.
The work factor is what makes these functions adaptive. As hardware gets faster over the years, you increase the cost parameter so that the per-guess time stays roughly constant despite improving attacker hardware. Argon2 and scrypt add a further dimension by being memory-hard: they require a tunable amount of memory as well as time, which blunts the advantage of specialised cracking hardware that has lots of parallel compute but limited fast memory. Choosing a sensible work factor — high enough to be slow, low enough not to overload your server — and revisiting it periodically is a key part of doing password hashing well.
How It Looks in Practice
You rely on a vetted library, which handles salting, the work factor and verification for you:
// Node.js with bcrypt
import bcrypt from "bcrypt";
// On registration: hash and store (salt is generated and embedded automatically)
const hash = await bcrypt.hash(password, 12); // 12 = cost factor
// On login: verify
const ok = await bcrypt.compare(password, hash);
# Python with Argon2
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash(password) # registration
ph.verify(hash, password) # login (raises on mismatch)
Notice you never see SHA-256 here, and you never manage the salt by hand — the library embeds the salt and parameters into the stored hash string so verification has everything it needs.
The Anatomy of a Password Breach
To see why every one of these practices matters, it helps to walk through what actually happens after a database is stolen, because the defences make far more sense when you picture the attack they are stopping. The moment an attacker obtains your user table, the passwords are no longer protected by your application, your firewall or your access controls — those are all bypassed. The only thing standing between the attacker and your users' plaintext passwords is the form in which those passwords were stored. If they were stored in plain text, the breach is total and instantaneous: every account is compromised the second the data is copied. This is why plain-text storage is indefensible.
If the passwords were stored as fast hashes like SHA-256, the situation is only marginally better. The attacker runs an offline cracking campaign, feeding enormous lists of common passwords, dictionary words and previously breached passwords through the same fast hash and comparing the results to your stored hashes. Because the hash is fast, they test billions of candidates per second, and the depressing reality is that a large fraction of real users choose weak, common passwords that fall almost immediately. Within hours, a substantial portion of the database is cracked. Now picture the same breach where passwords were stored with a properly tuned Argon2 or bcrypt: each guess takes a meaningful fraction of a second, the attacker's rate collapses from billions to a few thousand per second, and unique salts mean they cannot attack all accounts at once or use precomputed tables. The same stolen file that yielded near-total compromise with a fast hash yields almost nothing against strong passwords. Every recommendation in this guide exists to engineer that second outcome instead of the first.
Choosing and Tuning Your Function
With the threat clear, the practical question is which function to choose and how to configure it. Argon2 is the current first recommendation, having been selected through the public Password Hashing Competition specifically for this purpose, and its memory-hardness makes it especially resistant to the specialised hardware attackers use. Where Argon2 is unavailable, bcrypt is a long-trusted and widely supported choice that remains perfectly acceptable, and scrypt and PBKDF2 are also sound, with PBKDF2 often chosen in environments that require a government-standardised algorithm. Any of these, configured well, is vastly better than a fast hash; the differences between them matter far less than the difference between using one of them and not.
Tuning comes down to the work factor, and the guiding principle is to make hashing as slow as you can tolerate without degrading the user experience or overloading your servers. A common target is to spend somewhere around a quarter of a second per password hash on your hardware, which is imperceptible to a user logging in but punishing to an attacker. Because hardware improves, this is not a set-and-forget decision: revisit the work factor periodically and increase it so that the per-hash time stays roughly constant against ever-faster attacker hardware. For memory-hard functions, also tune the memory parameter, since it is the memory requirement that most blunts specialised cracking rigs. The reassuring part is that the libraries make all of this straightforward, exposing simple cost parameters and handling the salting and verification details for you.
More Than Hashing: The Full Picture
Strong hashing is necessary but works best alongside other measures. Always transmit passwords over HTTPS so they are encrypted in transit. Verify passwords using the library's constant-time comparison to avoid timing attacks that could leak information about the hash. Consider a server-side pepper — a secret value added to all passwords and stored separately from the database — so that a database-only breach does not hand over everything. Encourage or require strong passwords and check them against lists of known-breached passwords. And enable multi-factor authentication, which means that even a cracked password alone is not enough to take over an account. Hashing protects the stored passwords; these measures protect the system around them.
A Note on Our SHA-256 Tool
Our SHA-256 Hash Generator is excellent for integrity checks, fingerprints and learning how hashing behaves — but, consistent with everything above, it is not the right tool for storing passwords. Use it to verify a download or experiment with hashing; use a dedicated password-hashing library in your application's authentication code.
👉 Explore hashing with our free SHA-256 tool →
Common Mistakes
- Storing passwords with SHA-256 or MD5. Fast hashes are brute-forced en masse after a breach.
- Storing passwords in plain text. The worst case — a breach exposes everything instantly.
- Using a single shared salt, or no salt. This re-enables precomputed and rainbow-table attacks.
- Rolling your own slow hash. Hand-built schemes miss subtleties; use proven functions.
- Never increasing the work factor. Costs set years ago become too weak as hardware improves.
- Relying on hashing alone. Without HTTPS, MFA and good password policy, hashing is only part of the defence.
Best Practices
- Use Argon2, scrypt, bcrypt or PBKDF2 — never a plain fast hash — for password storage.
- Let the library handle per-password salts automatically.
- Set a strong work factor and increase it over time as hardware improves.
- Verify with constant-time comparison and transmit over HTTPS.
- Consider a server-side pepper stored separately from the database.
- Layer on MFA and strong-password checks for defence in depth.
Frequently Asked Questions
Can I use SHA-256 to hash passwords?
No. SHA-256 is far too fast, so a stolen database of SHA-256 password hashes can be brute-forced at billions of guesses per second. Use a slow, salted password-hashing function such as Argon2, scrypt, bcrypt or PBKDF2.
What is the best password hashing algorithm?
Argon2 is the current recommended choice, having won a public password-hashing competition. scrypt, bcrypt and PBKDF2 are also well-established and acceptable. All are slow, salted and adaptive by design.
Why do I need a salt?
A unique, random salt per password ensures that identical passwords produce different hashes and defeats precomputed and rainbow-table attacks. The salt does not need to be secret, only unique and random.
What is a work factor?
It is a parameter that controls how expensive the hash is to compute. You set it high enough that each hash takes a noticeable fraction of a second, which barely affects users but cripples brute-force attackers, and you raise it over time as hardware improves.
Should passwords ever be stored in plain text?
Never. Plain-text storage means a single breach exposes every password immediately. Passwords must always be stored as a slow, salted hash.
Is hashing enough to secure passwords?
It is essential but not sufficient on its own. Combine strong password hashing with HTTPS, constant-time verification, optional peppering, strong-password checks and multi-factor authentication.
Summary
Password storage is the one place where a fast cryptographic hash like SHA-256 is exactly the wrong tool. Passwords demand slow, salted, adaptive functions — Argon2, scrypt, bcrypt or PBKDF2 — because the only defence after a database breach is how long it takes an attacker to guess each password, and slowness is what buys that time. Let a proven library handle the salting and work factor, never store plain text or use a single salt, raise the cost as hardware advances, and surround hashing with HTTPS, constant-time checks, optional peppering and multi-factor authentication. Use general hashes like SHA-256 for integrity and a dedicated password hash for passwords, and you will avoid the most damaging authentication mistake in software. It is a rare case where one clear rule, correctly applied, protects every user you will ever have — so make it a non-negotiable standard in everything you build.
The encouraging takeaway is that doing password storage correctly is not hard — it is just specific. You do not need to be a cryptographer or to understand the internals of Argon2; you need to use the right library, pick one of the recommended functions, set a sensible work factor, and let the library handle salting and verification. The danger lies entirely in not knowing the rule, in the well-meaning developer who reaches for the familiar SHA-256 because it is the hash they know. Once you internalise that passwords belong to a different category requiring slow, salted, purpose-built functions, the implementation is a few lines of well-trodden code. That single piece of knowledge — fast hashes for integrity, slow hashes for passwords — prevents one of the most common and most catastrophic security mistakes in all of software, and it costs nothing to apply once you know it.
👉 Learn hashing with our free SHA-256 tool →
Related Resources
- What Is SHA-256? — the fundamentals
- SHA-256 vs MD5 — why both are wrong for passwords
- Modern Cryptographic Hashes — the wider landscape
- JWT vs Session Authentication — related auth topic
- SHA-256 Hash Generator — the tool