Password Hashing Explained: bcrypt, scrypt, and Argon2
The property that makes SHA-256 great for checksums — it's extremely fast — is exactly what makes it a bad choice for passwords. The algorithms built for password storage are deliberately, tunably slow.
Published July 6, 2026If a database of password hashes leaks, the attacker's job is to guess passwords and hash each guess until one matches. How long that takes depends almost entirely on how fast the hash function is. A modern GPU can compute billions of SHA-256 hashes per second, which means a leaked table of SHA-256(password) values can have most common passwords cracked within hours, salt or no salt. Password hashing algorithms exist specifically to make that guessing loop expensive, one guess at a time, even for an attacker with serious hardware.
Salting: solving a different problem
A salt is random data mixed into the password before hashing, unique per user and stored alongside the hash. It doesn't slow down cracking a single password, but it defeats precomputed rainbow tables and stops an attacker from noticing that two users share the same password (their hashes would otherwise be identical). Salting is necessary but not sufficient — you still need the hash function itself to be slow, or a fast GPU brute-forces each salted hash individually just as easily.
bcrypt
bcrypt, based on the Blowfish cipher, has been the standard choice for over two decades. Its defining feature is a tunable cost factor that controls how many rounds of internal key setup it performs — each increment doubles the work:
import bcrypt
hashed = bcrypt.hashpw(b"correct horse battery staple", bcrypt.gensalt(rounds=12))
# store `hashed` in the database
bcrypt.checkpw(b"correct horse battery staple", hashed) # True
bcrypt.checkpw(b"wrong guess", hashed) # False
bcrypt.gensalt() generates and embeds a random salt automatically, so the salt travels with the hash and never needs separate storage or handling. A cost factor of 12 (the common default) means roughly 4,096 rounds of internal computation per hash — slow enough to matter across billions of guesses, fast enough that one legitimate login check is imperceptible to a real user.
scrypt: adding memory cost
bcrypt is CPU-hard but not memory-hard, which means custom ASIC or FPGA hardware can still parallelize cracking attempts far more cheaply than a general-purpose CPU can. scrypt was designed specifically to also require a large, tunable amount of memory per hash computation, which makes that kind of specialized hardware attack much less economical — building a chip with more compute cores is cheap, building one with proportionally more fast memory per core is not.
Argon2: the current recommendation
Argon2 won the Password Hashing Competition in 2015 and is now the algorithm security guidance generally recommends by default. It comes in three variants — Argon2d (maximizes resistance to GPU cracking), Argon2i (resists side-channel timing attacks), and Argon2id (a hybrid, and the recommended default for password storage). It exposes three separate tunable costs: memory, iterations, and parallelism, giving finer control than bcrypt's single cost factor.
from argon2 import PasswordHasher
ph = PasswordHasher() # sane defaults: Argon2id, tuned memory/time cost
hashed = ph.hash("correct horse battery staple")
try:
ph.verify(hashed, "correct horse battery staple")
print("password matches")
except Exception:
print("password does not match")
Concrete parameter guidance — recommended memory size, iteration count, and how to choose between bcrypt, scrypt, and Argon2id for a given threat model — is maintained in the OWASP Password Storage Cheat Sheet, which is worth reading directly rather than relying on defaults that may be years out of date by the time you deploy.
What none of these algorithms do
Password hashing protects against an offline attacker who already has your hash database. It does nothing against a weak password guessed in an online attack against the login form itself (rate limiting and account lockout handle that), and it does nothing if the application logs plaintext passwords, sends them in cleartext, or stores them anywhere before hashing happens. The algorithm choice matters, but it's one control in a system that also needs TLS in transit, secrets kept out of source control, and rate limiting on the login endpoint to be actually secure.