A startup's entire user table leaked last year because their "senior" dev hashed passwords with SHA-256 and a static salt. The attacker cracked 80% of the passwords in under six hours using a single GPU. SHA-256 is a general-purpose hash — it's designed to be fast, and fast is exactly what you don't want when hashing passwords.
Use Argon2id in 2025. It won the Password Hashing Competition in 2015, it's recommended by OWASP, and it's the only algorithm in this group designed specifically to resist both GPU-based brute force and side-channel attacks. If you're stuck on an older codebase running bcrypt, stay on it — just increase the work factor. But for any greenfield work, Argon2id is the right call.
Why "slow by design" is the whole point
Regular hash functions — SHA-256, MD5, BLAKE3 — are optimized for throughput. A modern GPU can compute billions of SHA-256 hashes per second. An attacker with a $500 GPU and a leaked database can tear through a poorly protected table in hours.
Password hashing algorithms deliberately consume CPU, memory, or both. The idea is to make each hash take 100–300ms on your server, which is imperceptible to a user logging in, but catastrophic for an attacker trying billions of candidates. The key tuning parameters are:
- Time cost — how many iterations to run
- Memory cost — how much RAM to allocate per hash (this is what kills GPU attacks — GPUs have fast cores but limited per-core memory bandwidth)
- Parallelism — how many CPU threads to use
bcrypt: still fine, but showing its age
bcrypt has been around since 1999 and has an excellent track record. The work factor (cost) is a log₂ value — cost 12 means 2¹² = 4096 iterations. OWASP recommends a minimum cost of 10, with 12 as a good default in 2025.
The problem: bcrypt truncates passwords at 72 bytes. If a user has a passphrase longer than 72 characters, everything after character 72 is silently ignored. Two different passwords that share the same first 72 bytes will produce the same hash. This is a real security issue that surprises a lot of developers.
The other limitation is memory. bcrypt uses a fixed 4KB of memory, which was meaningful in 1999 but is trivial for modern GPUs. It offers no memory-hard guarantee in 2025.
When to keep using bcrypt: existing systems where migration is risky, or ecosystems where Argon2 support is immature.
scrypt: memory-hard, but harder to tune correctly
scrypt was introduced in 2009 specifically to address bcrypt's GPU vulnerability by adding memory hardness. It has three parameters: N (CPU/memory cost), r (block size), and p (parallelism). The interaction between these parameters is non-obvious, and misconfiguring them can produce either a dangerously weak hash or one that OOMs your server.
OWASP's current recommendation for scrypt is N=32768, r=8, p=1, which requires about 32MB of RAM per hash. That's workable, but Argon2 gives you better tuning flexibility and stronger security guarantees with simpler parameters.
scrypt is a reasonable choice in environments where Argon2 isn't available (some older PHP environments, for example), but it's not the first pick for new work.
Argon2: the right choice for new systems
Argon2 comes in three variants:
- Argon2d — maximizes GPU resistance, but vulnerable to side-channel attacks. Don't use for password hashing.
- Argon2i — side-channel resistant, less GPU-resistant. Use for key derivation from untrusted inputs.
- Argon2id — hybrid of both. Use this for password hashing.
OWASP's 2025 recommended minimum parameters for Argon2id: memory=19MB (19456 KiB), iterations=2, parallelism=1. For higher-security systems, push memory to 64MB+ and iterations to 3–4. Benchmark on your target hardware and tune until hashing takes 100–300ms.
The memory parameter is what makes Argon2id resistant to GPU attacks. A GPU might have thousands of cores, but each core has very limited memory — forcing 64MB per hash makes parallelizing an attack on a GPU extremely expensive.
The rehashing upgrade path
If you're running a system with bcrypt hashes and want to migrate to Argon2id without forcing a password reset, do it transparently on login:
- User logs in — you receive their plaintext password
- Verify against the existing bcrypt hash
- If it passes, immediately hash the password with Argon2id and overwrite the stored hash
- Over time, active users get upgraded; inactive accounts stay on bcrypt until they next log in (still protected)
This is standard practice and requires zero user action. You can track the algorithm in a separate column or infer it from the hash prefix ($2b$ for bcrypt, $argon2id$ for Argon2id).
One thing most developers miss
Argon2id encodes the salt, parameters, and algorithm version directly into the output string. You do not need to store the salt separately. The encoded hash is self-contained — this is why you can call verify() without passing parameters explicitly; they're parsed from the hash string itself.
This also means if you change your Argon2id parameters (say, increasing memory cost after a hardware upgrade), old hashes remain verifiable. The parameters embedded in the hash are used for verification; new hashes will use the updated settings. You can layer in a needsRehash() check to upgrade old hashes on next login.
Storing the salt separately and re-deriving it is a pattern from older systems — with Argon2id, you just store the full encoded string and let the library handle the rest.
Picking parameters for your environment
Don't just copy OWASP's minimum values and ship them — benchmark on your actual hardware. Both argon2 (Node) and argon2-cffi (Python) expose timing utilities. Target 150–250ms per hash operation under your expected concurrent load. If you're on a memory-constrained container (256MB pods are common in cheap Kubernetes setups), test that hashing under load doesn't OOM your instances.
Best choice for new systems. Memory-hard, side-channel resistant.
For most web APIs, Argon2id with memory=64MB, iterations=3, parallelism=1 is a comfortable default on modern hardware — it's significantly stronger than the OWASP minimum and still well within typical server specs.
Open the password strength checker and test the passwords your users are actually submitting — you might be surprised how many "strong" passwords collapse under real analysis.
Frequently Asked Questions
Related posts
Secure Password Reset Tokens — Expiry, Storage, and What Most Implementations Get Wrong
A practical guide to building secure password reset flows: token generation, expiry windows, one-time use enforcement, and the edge cases that cause real account takeovers.
Mar 30, 2026 · 7 min readIncident Response for Developers: What to Do When You Get Hacked
A practical incident response guide for developers covering detection, containment, eradication, recovery, and communication when a security breach happens.
Mar 29, 2026 · 9 min readPhishing Prevention: A Developer's Guide to SPF, DKIM, and DMARC
Understand how email spoofing enables phishing attacks and how to implement SPF, DKIM, and DMARC to protect your domain from being impersonated.
Mar 29, 2026 · 9 min read