bcrypt vs Argon2 vs scrypt vs PBKDF2: Which Password Hashing Algorithm Should You Use?
2026-10-11 · 9 min read
Figure 1: The four password hashing algorithms compared
Description
If you store passwords, you should never keep them as plain text or as a fast digest like MD5 or SHA-256. Fast hashes are built for speed, and speed is exactly what helps an attacker who has stolen your database guess billions of candidates every second.
The right tool is a password hashing function, also called a key derivation function (KDF). A good KDF is deliberately slow and tunable. The four you will meet in real projects are bcrypt, Argon2, scrypt, and PBKDF2.
This guide explains how each option works, where it is strong or weak, what settings to apply today, and how to choose between them. Every section includes a short Python example you can run yourself.
Why "slow" is the whole point
A password hashing function has one job beyond producing a digest: staying expensive to run. Every algorithm here exposes a work factor you can raise as hardware improves, so verifying one login stays quick (a few hundred milliseconds) while guessing millions of candidates stays painfully costly for an attacker.
They differ mainly in what kind of cost they impose:
- CPU-bound: burns processor time. PBKDF2 and bcrypt behave this way.
- Memory-bound: forces the attacker to spend plenty of RAM per guess, which defeats cheap parallel cracking on GPUs and custom ASIC or FPGA rigs. scrypt and Argon2 behave this way.
Memory cost is the modern dividing line, because the biggest threat today is massively parallel GPU cracking.
PBKDF2
PBKDF2 (Password-Based Key Derivation Function 2) is the oldest of the four and ships in a public standard (RFC 8018). It repeatedly applies an HMAC, usually HMAC-SHA256 or HMAC-SHA512, for a configurable iteration count.
- Strengths: simple, available everywhere, and FIPS-140 approved, which matters for US government and regulated environments. Nearly every language bundles it in the standard library.
- Weaknesses: CPU cost only. It carries no memory requirement, so GPUs and ASICs crack it far more efficiently than a defender's server runs it. The iteration count must be very high to stay respectable.
- Recommended settings: OWASP advises roughly 600,000 iterations for PBKDF2-HMAC-SHA256, or about 210,000 for PBKDF2-HMAC-SHA512. Confirm the latest figures, since they climb over time.
PBKDF2 in Python
No install needed. PBKDF2 lives in the standard library hashlib module.
import hashlib
import hmac
import os
import binascii
password = b"S3cur3P@ss"
salt = os.urandom(16)
iterations = 600_000
dklen = 32 # SHA-256 produces a 32-byte digest
def pbkdf2_hmac_sha256(password, salt, iterations, dklen):
hash_len = 32
blocks = (dklen + hash_len - 1) // hash_len
derived = bytearray()
for block_index in range(1, blocks + 1):
u = hmac.new(
password,
salt + block_index.to_bytes(4, "big"),
hashlib.sha256
).digest()
result = bytearray(u)
for _ in range(iterations - 1):
u = hmac.new(password, u, hashlib.sha256).digest()
for i in range(hash_len):
result[i] ^= u[i]
derived.extend(result)
return bytes(derived[:dklen])
derived = pbkdf2_hmac_sha256(password, salt, iterations, dklen)
print("salt:", binascii.hexlify(salt).decode())
print("hash:", binascii.hexlify(derived).decode())
Pick PBKDF2 when FIPS compliance or a limited platform forces your hand.
bcrypt
bcrypt builds on the Blowfish cipher and has served as the pragmatic default for password storage for more than two decades. It salts every digest automatically and packs everything (version, cost, salt, and result) into a single $2b$ string.
- Strengths: proven in production, automatic per-password salting, and a single tunable cost factor. Its Blowfish memory access pattern makes it somewhat tougher on GPUs than PBKDF2.
- Weaknesses: no real memory requirement, plus a well-known quirk. It reads only the first 72 bytes of input and silently drops the rest. Some builds also stop at the first null byte.
- Recommended settings: OWASP suggests a cost factor of 10 or higher. Tune it so one digest takes a few hundred milliseconds on your hardware. For long passphrases, pre-hash with SHA-256 before bcrypt to sidestep the 72-byte ceiling.
bcrypt in Python
Install the library first:
pip install bcrypt
Then hash and verify. gensalt builds a random salt with your chosen cost, and checkpw compares a candidate against the stored value.
import bcrypt
password = "S3cur3P@ss".encode("utf-8")
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed.decode())
# at login time
print(bcrypt.checkpw(password, hashed)) # True
print(bcrypt.checkpw(b"wrong", hashed)) # False
Pick bcrypt when Argon2 is missing from your stack and you want a low-risk, well-tested choice.
scrypt
scrypt was designed to lean on memory by intent. It forces the computation to hold a large block of RAM, so an attacker cannot cheaply trade memory for speed. Cryptocurrency mining made it widely known, though password storage was the original target.
- Strengths: heavy memory use, which blunts GPU and ASIC attacks far better than bcrypt or PBKDF2 can.
- Weaknesses: three interacting parameters (
Ncost,rblock size,pparallelism) make misconfiguration easy. Balancing RAM against processor time takes extra care. - Recommended settings: OWASP points to values around N = 2^17 (131072), r = 8, p = 1, tuned to your hardware.
scrypt in Python
No install needed, since hashlib ships scrypt. Note the maxmem argument: high N values need a raised memory ceiling or the call fails.
import hashlib, os, binascii
password = "S3cur3P@ss".encode("utf-8")
salt = os.urandom(16)
derived = hashlib.scrypt(
password,
salt=salt,
n=2**17, r=8, p=1, # OWASP baseline
dklen=32,
maxmem=2**30, # ~1 GiB cap, enough for these params
)
print("hash:", binascii.hexlify(derived).decode())
Pick scrypt when you want a memory-bound option but cannot reach for Argon2.
Argon2
Argon2 won the 2015 Password Hashing Competition and now stands as the recommended default. It leans heavily on memory, exposes clean parameters, and ships in three variants:
Argon2id resists both side-channel and GPU attacks. This is the one to deploy.
Argon2i uses data-independent memory access for side-channel resistance.
Argon2d uses data-dependent access for maximum GPU resistance, at the cost of side-channel exposure.
Strengths: current best-in-class memory cost, independent control over RAM, iterations, and parallelism, plus a clear default variant in Argon2id.
Weaknesses: younger than the rest, so a few older or restricted platforms still lack a vetted library. It also sits outside FIPS approval.
Recommended settings: OWASP suggests Argon2id with at least 19 MiB of RAM, 2 iterations, and 1 degree of parallelism as a baseline. A common alternative uses 46 MiB with 1 iteration. Raise the values to fit your latency budget.
Argon2 in Python
Install the argon2-cffi library:
pip install argon2-cffi
PasswordHasher salts automatically and returns a self-describing PHC string. memory_cost is in KiB, so 19456 equals 19 MiB.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1)
stored = ph.hash("S3cur3P@ss")
print(stored) # $argon2id$v=19$m=19456,t=2,p=1$...
# at login time
print(ph.verify(stored, "S3cur3P@ss")) # True
try:
ph.verify(stored, "wrong")
except VerifyMismatchError:
print("password does not match")
Pick Argon2id when your platform supports it. For a fresh application in 2026, this is the default.
Side-by-side comparison
| Feature | PBKDF2 | bcrypt | scrypt | Argon2id |
|---|---|---|---|---|
| Year | 2000 | 1999 | 2009 | 2015 |
| Cost type | CPU-bound | CPU-bound | Memory-bound | Memory-bound |
| Heavy RAM use | No | No | Yes | Yes |
| Auto-salt | No (you add it) | Yes | No (you add it) | Yes |
| FIPS approved | Yes | No | No | No |
| Main tuning knob | iterations | cost factor | N, r, p | memory, time, parallelism |
| Known gotcha | GPU-friendly | 72-byte limit | easy to misconfigure | fewer FIPS options |
| GPU and ASIC resistance | Low | Medium | High | Highest |
A quick decision guide
- Starting fresh? Reach for Argon2id. It reflects current best practice.
- Need heavy RAM cost but no Argon2 library? Use scrypt.
- No modern option, but want something solid? Use bcrypt, and pre-hash long passwords.
- Bound by FIPS-140 or similar rules? Use PBKDF2-HMAC-SHA256 with a high iteration count.
Whatever you choose, the ground rules hold. Apply a unique random salt per password (the strong algorithms handle this for you), tune the work factor so one digest runs in a few hundred milliseconds on your real hardware, and raise that factor every couple of years.
What to avoid
- MD5, SHA-1, or SHA-256 alone: far too quick for passwords. Fine for checksums and integrity, wrong for credential storage.
- Encoding such as Base64 or hex: not protection at all, since anyone can reverse it.
- Home-rolled schemes: "SHA-256 looped a thousand times" is just a weaker PBKDF2. Reach for a vetted library instead.
Unsure how a one-way digest differs from reversible ciphers? Start with our explainer on hashing vs encryption vs encoding.
Final thoughts
All four algorithms are genuine password hashing functions, and any of them beats a bare SHA-256. What separates them is how well they hold up against modern parallel cracking:
- Argon2id is the default to reach for first.
- scrypt is the memory-bound fallback.
- bcrypt is the trusted older workhorse.
- PBKDF2 is the compliance-driven pick.
Test every algorithm right in your browser with the KeyDecryptor hash tools linked above. Each one runs client-side, so nothing you type ever leaves your machine.