Password Hashing in 2026: bcrypt vs Argon2 vs scrypt vs PBKDF2 β A Practitioner's Guide
Choosing a password hashing algorithm in 2026 isn't obvious: bcrypt is still safe but no longer state-of-the-art, Argon2id is now the OWASP recommendation, scrypt is memory-hard but leapfrogged, and PBKDF2 is the FIPS-approved holdout. This guide walks through the four contenders with working parameters, migration strategies for switching algorithms without breaking auth, and the common mistakes that turn good algorithms into bad implementations.
Three things changed for password hashing between 2020 and 2026. The OWASP Password Storage Cheat Sheet got its biggest rewrite in years and moved Argon2id to the top of the recommendation list. Argon2's RFC 9106 was published in September 2021, ending years of "is it standardized yet?" hesitation. And the gap between bcrypt (still safe) and Argon2 (now preferred) became more about long-term direction than urgent migration.
This guide is for the engineer who knows they need to hash passwords but isn't sure which algorithm to pick, what parameters to use, and how to migrate from whatever they have today to whatever they should have tomorrow β without breaking auth for every user.
Why fast hashes lose
The most common password-hashing mistake isn't picking the wrong slow hash β it's using a fast hash. SHA-256 evaluates in under a microsecond on a modern CPU. Modern GPUs hit several billion SHA-256 evaluations per second. A leaked SHA-256-hashed password database with no salt is broken for any password under 10 characters within hours.
The entire point of a password hash is to be slow on purpose. Not slow as a bug, slow as a feature. When you increase the work factor, you trade login latency (a few hundred milliseconds, paid once per session) for attacker brute-force cost (now measured in days or years per leaked database, depending on parameters).
π‘ Just need to hash a password right now? Use our bcrypt Hash Generator, Argon2 Hash Generator, scrypt Hash Generator, or PBKDF2 Hash Generator to get a properly-formatted hash with sensible defaults. The rest of this article walks through which algorithm to pick for which use case.
What makes a good password hash β five properties
Every modern password-hashing algorithm trades off the same five properties. Understanding them is what lets you read the next round of recommendations (whenever it ships) and make sense of it.
1. Intentionally slow. Tunable via a work factor (bcrypt cost, PBKDF2 iterations, Argon2 time cost, scrypt N). The right setting is the largest one your latency budget tolerates: hundreds of milliseconds for a normal web login, a few seconds for a CLI tool or password manager. The number itself isn't the point β the time it takes on your hardware is. Re-benchmark whenever you change deployment platform.
2. Memory-hard. Modern requirement, only Argon2 and scrypt satisfy it. Memory-hardness means the algorithm needs hundreds of megabytes of RAM per evaluation β which is fine for a server (do it once per login) but impossible to scale on a GPU farm (GPUs have limited per-core memory). This is why post-2018 algorithms beat bcrypt against well-funded attackers: bcrypt's 4KB working set fits in any GPU core.
3. Per-password salt. Minimum 16 bytes random per user, generated by a cryptographic RNG. Salt defeats rainbow tables and forces attackers to brute-force each hash individually. Modern hash libraries generate salt automatically; older code sometimes left it as a manual step. Never hardcode, never reuse, never store in a separate column.
4. Tunable as hardware improves. The work factor must be a parameter, not a constant. A cost factor of 10 was reasonable in 2015 and inadequate by 2020. If you can't increase it without forcing every user to reset their password, the algorithm has failed at its job. Modern algorithms encode the work factor in the hash output itself, which is what lets you rotate parameters gradually.
5. Self-describing encoded output. The hash string contains everything needed to verify it: algorithm identifier, version, parameters, salt, and the hash bytes. Examples: $2b$12$... (bcrypt cost 12), $argon2id$v=19$m=65536,t=3,p=1$... (Argon2id with specific parameters). Future-you can read the stored hash and dispatch to the correct verifier without consulting a separate config table.
The four contenders
bcrypt
TL;DR: Still solid, just no longer state-of-the-art. Use if you need broad library support or already have it deployed.
History. Designed by Niels Provos and David MaziΓ¨res in 1999 for OpenBSD, bcrypt was the first widely-deployed adaptive password hash. The cost factor design β increase the parameter, increase the work β set the template every later algorithm followed. Two decades of production use across virtually every language and framework, no successful attacks against the algorithm itself.
How it works. Bcrypt builds on the Blowfish cipher's key schedule, deliberately the slowest part of Blowfish. The cost factor controls how many times the key schedule runs (2^cost iterations). Each evaluation needs about 4KB of working memory β small enough to fit in CPU cache, which is exactly what makes bcrypt GPU-friendly. The hash output is fixed at 60 characters: $2b$cost$22-char-salt53-char-hash.
Parameters in 2026. Cost factor 12 is the floor. Cost 13 or 14 is common in security-conscious production. The cost is exponential β going from 12 to 13 doubles the work. Benchmark on your server hardware: cost 12 typically runs 250β400ms on a modern x86 CPU. Bcrypt has a hard 72-byte password length limit; longer passwords are silently truncated, so most libraries pre-hash with HMAC-SHA256 if length might exceed 72 bytes.
When to use it. New projects that need broad legacy ecosystem support (PHP shared hosting, old enterprise Java stacks). Existing bcrypt deployments at cost β₯12 β no urgency to migrate. Cross-platform code where Argon2 bindings are inconsistent.
When NOT to use it. Greenfield projects with no compatibility constraints β Argon2id is the better default. Anywhere GPU/ASIC brute force is a realistic threat model (high-value targets, password databases that might leak).
Working example (Python):
import bcrypt
# Hashing at registration
password = b"correct horse battery staple"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# Store `hashed` (bytes) in the database
# Verifying at login
if bcrypt.checkpw(password, hashed):
print("auth ok")
Try it in your browser: bcrypt Hash Generator & Verifier β generate a bcrypt hash with adjustable cost factor, or paste an existing hash to verify against a password.
PBKDF2
TL;DR: The FIPS-approved holdout. Wins on compliance, loses on memory-hardness.
History. Defined by RSA Laboratories in PKCS #5 (2000), formalized by RFC 8018, and explicitly approved by NIST SP 800-132 and the FIPS 140 cryptographic module standard. PBKDF2's longevity comes entirely from its compliance status β it's the only password hash on the approved list for government, regulated finance, and healthcare workloads where "we use a NIST-approved KDF" is a literal audit requirement.
How it works. PBKDF2 runs a pseudorandom function (almost always HMAC-SHA256 in modern deployments; sometimes HMAC-SHA1 or HMAC-SHA512) over the password and salt for a configurable number of iterations. Each iteration is fast β that's PBKDF2's weakness, not strength. To make brute force expensive, you must crank iterations very high.
Parameters in 2026. OWASP's current PBKDF2-SHA256 recommendation is 600,000 iterations minimum. PBKDF2-SHA512 is acceptable at 210,000 iterations. The salt should be 16 bytes random. The output length should match your storage column (usually 32 bytes). PBKDF2 uses no memory beyond the PRF state, which is why GPUs and ASICs make short work of it given enough iterations β the iteration count must be much higher than bcrypt's cost-12 equivalent to provide similar security.
When to use it. FIPS 140-2 / FIPS 140-3 environments. SOC 2 audits where the auditor specifically asks for an "approved KDF." HIPAA / PCI / FedRAMP environments that follow NIST SP 800-132 to the letter. Anywhere a security team's auditor reads "Argon2" and asks "what's that?" β sometimes the right answer is "we picked the approved one and configured it tightly."
When NOT to use it. Default-by-choice. If you're not bound by compliance, Argon2id is straightforwardly better against modern attacks.
Working example (Node.js):
const crypto = require('crypto');
// Hashing
const password = 'correct horse battery staple';
const salt = crypto.randomBytes(16);
const iterations = 600000;
const hash = crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256');
// Store: `$pbkdf2-sha256$i=${iterations}$${salt.toString('base64')}$${hash.toString('base64')}`
// Verifying (constant-time)
const check = crypto.pbkdf2Sync(input, salt, iterations, 32, 'sha256');
const ok = crypto.timingSafeEqual(hash, check);
Try it in your browser: PBKDF2 Hash Generator & Verifier β generate PBKDF2 hashes with configurable iterations and SHA variants.
scrypt
TL;DR: Memory-hard pioneer, but Argon2 implemented the same idea better.
History. Designed by Colin Percival in 2009 specifically to defeat GPU and ASIC attacks against bcrypt and PBKDF2. Scrypt introduced sequential memory-hardness β an attacker who wants to evaluate scrypt in parallel must scale memory too, which GPU/ASIC architectures can't do cheaply. RFC 7914 standardized it in 2016. Tarsnap's backup service is the most famous deployment.
How it works. Scrypt has three parameters: N (CPU/memory cost, must be a power of 2), r (block size), p (parallelization). Internally it generates a large random-access array of size proportional to N Γ r, then walks the array in a pattern that depends on intermediate state. The array doesn't fit in any reasonable GPU memory, so parallel attacks lose their cost advantage.
Parameters in 2026. OWASP recommends N=2^17 (131072), r=8, p=1, which uses roughly 128 MB per hash. That's the "high-end" parameter set. The "moderate" set is N=2^16, r=8, p=1 at 64 MB. Adjust N until hash time hits 250β400ms on your server. P (parallelization) at 1 is the right default β increasing it speeds up evaluation but reduces memory-hardness.
When to use it. Existing scrypt deployments β don't migrate just because Argon2 exists. Scrypt isn't broken, it's just been leapfrogged. Anywhere RFC 7914 specifically is required (rare).
When NOT to use it. New projects in 2026. Argon2id solves the same problem with a more recent standard, cleaner parameter tuning, and better library support.
Working example (Python):
import hashlib
import os
# Hashing
password = b"correct horse battery staple"
salt = os.urandom(16)
hash_bytes = hashlib.scrypt(password, salt=salt, n=131072, r=8, p=1, dklen=64)
# Store the encoded form: $scrypt$n=17,r=8,p=1$base64(salt)$base64(hash)
# Verifying
check = hashlib.scrypt(input, salt=stored_salt, n=stored_n, r=stored_r, p=stored_p, dklen=64)
ok = hmac.compare_digest(hash_bytes, check)
Try it in your browser: scrypt Hash Generator & Verifier β generate scrypt hashes with configurable N, r, and p parameters.
Argon2
TL;DR: OWASP 2024+ default. The right answer for new applications in 2026.
History. Argon2 won the Password Hashing Competition (PHC) in July 2015, beating eight other finalists in a multi-year academic and industry review. RFC 9106 standardized it in September 2021. OWASP's 2024 Password Storage Cheat Sheet update moved Argon2id to the top recommendation. Three variants exist: Argon2d (data-dependent, fastest, vulnerable to side-channel), Argon2i (data-independent, slowest, immune to side-channel), and Argon2id (hybrid).
How it works. Argon2 builds a large memory matrix proportional to the memory parameter, then iterates over it for the time parameter. Each iteration depends on the previous state and on memory locations chosen pseudorandomly. Like scrypt, this makes parallel GPU/ASIC attacks expensive β but Argon2's tuning is cleaner (m, t, p map directly to memory, time, parallelism) and the spec is post-PHC reviewed.
Parameters in 2026. For Argon2id (the variant you almost always want): memory cost m=64 MB (m=65536 KiB), time cost t=3, parallelism p=1. That's the OWASP 2024 recommended profile. The "high" profile is m=128 MB, t=4, p=1. Salt size 16 bytes, output hash 32 bytes. Benchmark on your server β these parameters give roughly 250β400ms on modern hardware. If you have very tight latency requirements, reduce time first (t=2 is acceptable) before reducing memory.
Why Argon2id over the others. Argon2d optimizes against GPU brute force but leaks timing information through data-dependent memory access. Argon2i resists timing attacks but is weaker against GPU attacks (it was famously partially broken by an academic team in 2016 β recovered with parameter changes, but the lesson stuck). Argon2id runs Argon2i for the first half-iteration (timing-resistant) and Argon2d for the rest (GPU-resistant). For password hashing β where both attack classes apply β id is the right hybrid. Use d only when deriving keys (KDF) where timing attacks aren't a concern.
When to use it. New applications. Migration target from bcrypt or scrypt. Anywhere you need state-of-the-art memory-hardness.
When NOT to use it. FIPS 140 environments (PBKDF2 instead). Constrained embedded environments without 64 MB available per concurrent auth (rare in 2026).
Working example (Python):
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
# Hashing
hashed = ph.hash("correct horse battery staple")
# Store `hashed` (string) β it's self-describing
# Verifying
try:
ph.verify(hashed, "correct horse battery staple")
print("auth ok")
except argon2.exceptions.VerifyMismatchError:
print("auth fail")
# Automatic rehash check (parameters changed since hash was created)
if ph.check_needs_rehash(hashed):
new_hashed = ph.hash("correct horse battery staple")
# update database
Try it in your browser: Argon2 Hash Generator & Verifier β generate Argon2id, Argon2i, or Argon2d hashes with full parameter control.
The 2026 decision matrix
The table below resolves the question for any common situation. Read it as a sanity check β if your situation doesn't fit, the section below the table explains the reasoning so you can extrapolate.
| Use case | Recommended | Parameters | Rationale |
|---|---|---|---|
| New web/mobile app, no compliance constraint | Argon2id | m=64 MB, t=3, p=1 | OWASP 2024 default |
| New app, broad legacy library support needed | bcrypt | cost=12 | Bcrypt libs exist everywhere |
| FIPS 140 / SOC 2 / HIPAA strict compliance | PBKDF2-SHA256 | 600,000+ iterations | Only NIST-approved option |
| Existing bcrypt at cost below 10 | Migrate to Argon2id (rehash on login) | m=64 MB, t=3, p=1 | Bcrypt safety threshold below 10 |
| Existing bcrypt at cost β₯12 | Keep bcrypt | Increase cost to 13 by 2027 | No urgency |
| Existing scrypt deployment | Keep scrypt | Current params | Not deprecated, just leapfrogged |
| Existing PBKDF2 with iterations below 100,000 | Increase iterations or migrate | 600,000+ or Argon2id | Underprovisioned |
| Embedded / IoT with limited RAM | bcrypt | cost matching available CPU | Argon2's memory cost too high |
How to benchmark on your hardware. Don't trust the numbers in any guide (including this one) without verifying on your actual production machines. The recipe:
import time
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
password = "benchmark"
start = time.perf_counter()
for _ in range(20):
ph.hash(password)
elapsed = (time.perf_counter() - start) / 20 * 1000
print(f"avg {elapsed:.0f}ms per hash")
Target the 250β400ms range for an interactive web login. Below 100ms and brute force is too cheap; above 1000ms and the login feels slow to users (and worse, a burst of concurrent logins can starve your CPU).
Burst load consideration. Argon2id at m=64 MB Γ 100 concurrent logins = 6.4 GB of memory pressure. Plan capacity accordingly. A common pattern is to run auth on its own pool of workers, sized for the worst-case concurrent login burst (think Monday morning password-reset spike). If memory pressure is a constraint, lower m to 32 MB and increase t proportionally β the security parameter is m Γ t, so trading one for the other preserves work.
Common mistakes that turn good algorithms into bad implementations
The algorithm choice is the easy part. The hard part is the dozen details around it. Every mistake below has been seen in real audits.
1. Reusing salt across users. The salt must be a fresh 16-byte random value per user, every time. A single salt across the database makes it equivalent to "no salt" β an attacker computes one rainbow table and breaks every user. Modern libraries generate salt automatically; the failure mode is older code that takes salt as an argument and someone hardcodes it for "consistency."
2. Insufficient cost factor. Bcrypt cost 10 was acceptable in 2015. Cost 10 in 2026 takes about 70ms on modern hardware β fast enough that brute force against leaked databases is realistic for any common password. Always start at cost 12 minimum, and review every 12-24 months.
3. Storing parameters in a separate column. The whole point of self-describing hash formats ($2b$12$...) is to let parameters change without schema changes. If you store the algorithm as one column and the hash as another, you've reinvented the worst part of legacy systems. Use a single VARCHAR(255) for the full encoded string.
4. Not preparing the code for migration. Auth code that hardcodes one algorithm is locked in until you rewrite it. The fix is one extra function: a prefix-based dispatcher that reads the hash string, determines the algorithm, and calls the correct verify(). With dispatch in place, switching algorithms is a deploy-and-wait operation; without it, every migration is a rewrite.
5. Using a password hash as a KDF for token derivation. Password hashing (Argon2id) is intentionally slow. Key derivation for short-lived tokens (HKDF, plain HMAC) is fast on purpose. Confusing them β using Argon2id to derive every API token, or using HKDF to hash a password β produces either unbearable latency or a broken security model.
6. Comparing hashes with == or string equality. Both bytewise comparison and string equality leak timing information. An attacker who can repeatedly call your verify endpoint can recover hashes byte-by-byte by measuring response latency. Always use the library's constant-time verify() or, if you must compare bytes manually, hmac.compare_digest() / crypto.timingSafeEqual().
7. Pepper without rotation strategy. Pepper sounds great in theory: a global secret value mixed in before hashing, stored outside the database. But pepper makes password rotation hard (you must keep old peppers to verify old hashes), and if the pepper leaks, you lose the benefit silently. Use pepper only when the operational complexity is justified β high-value applications where defense-in-depth against database-only leaks matters. For most apps, strong algorithm + strong parameters + breach detection beats adding pepper.
Migration strategy β switching algorithms without breaking auth
You can't ask every user to log in tomorrow and trigger a forced password reset. The migration must be transparent: on next login, the user authenticates against the old hash, and you transparently rehash with the new algorithm before they leave the request.
The wrap-and-rehash pattern.
def verify_and_maybe_rehash(password, stored_hash):
# Step 1: detect algorithm from hash prefix
if stored_hash.startswith("$2b$") or stored_hash.startswith("$2a$"):
algorithm = "bcrypt"
ok = bcrypt.checkpw(password.encode(), stored_hash.encode())
elif stored_hash.startswith("$argon2id$"):
algorithm = "argon2id"
ok = argon2_verify(stored_hash, password)
else:
raise ValueError(f"Unknown hash format: {stored_hash[:10]}")
if not ok:
return False, None
# Step 2: if it's not the current algorithm + parameters, rehash
target_algorithm = "argon2id"
target_params = {"time_cost": 3, "memory_cost": 65536, "parallelism": 1}
needs_rehash = (
algorithm != target_algorithm
or argon2_needs_rehash(stored_hash, **target_params)
)
if needs_rehash:
new_hash = argon2_hash(password, **target_params)
return True, new_hash # caller writes new_hash to DB
return True, None
Each successful login does the work-once migration for that user. Over weeks and months, the user base drifts to the new algorithm without any user action. No password resets, no support tickets, no user-visible change.
Storage format design.
$2b$12$saltsaltsaltsa22charshashashashasham <- bcrypt cost 12
$argon2id$v=19$m=65536,t=3,p=1$salt$hash <- Argon2id default
$pbkdf2-sha256$i=600000$salt$hash <- PBKDF2 600k (custom format)
$scrypt$n=17,r=8,p=1$salt$hash <- scrypt (custom format)
Bcrypt and Argon2 have standardized self-describing formats. PBKDF2 and scrypt don't β most languages use library-specific or framework-specific encodings. Pick one canonical encoding for each algorithm in your codebase, document it, and use it everywhere.
Timeline for a typical production migration.
- Week 1-4 (deployment): Ship dual-verification code. Reads either algorithm, writes new on successful login. Monitor: what fraction of logins are hitting the legacy path?
- Months 2-12 (natural rehashing): Active users gradually migrate. Daily-active users complete in weeks. Monthly-active users complete in months. After 12 months, the legacy-hash population is mostly users who have stopped logging in.
- Month 12 (forced reset for inactive): Email password-reset link to users with legacy hashes who haven't logged in for 90+ days. Most either reset or churn out naturally.
- Month 18+ (legacy removal): Remove the legacy verifier code. Any account still on the old hash must reset to log in.
What to do for forgotten accounts. Some users will never come back. The right policy is a 12-month inactivity grace period, then a mandatory password reset email at month 13, then account deactivation at month 18. This is good security hygiene regardless of password hashing β inactive accounts are an attack surface β so the policy serves double duty.
The complete password security stack β adjacent tools
Hashing is one layer. The full picture includes generation, strength checking, breach detection, and key management. Each of these lives in a separate tool, and treating them as a stack rather than isolated features is what separates "we hash passwords" from "we have a password security model."
Generating strong passwords. At registration, for service accounts, for one-time tokens, for password resets. A cryptographic password generator outputs uniformly distributed random characters from a configurable alphabet β far stronger than user-chosen passwords against guessing attacks.
Try it: Random Password Generator β generate cryptographically secure passwords with custom length, character sets, and exclusion rules. Use it for service accounts, default credentials, and one-time tokens.
Checking user password strength. When users choose their own password, you need a strength check that catches common patterns (dictionary words, sequential characters, leetspeak substitutions). The zxcvbn library is the industry standard β it gives an estimated crack-time rather than a meaningless "score from 1 to 5."
Try it: Password Strength Checker β zxcvbn-style strength analysis with crack-time estimation in human-readable units. Use it as the basis for your client-side registration gate.
Identifying unknown hashes. Found a hash in a breach dump, legacy database, or audit log? Before you can decide what to do with it, you need to know what it is. Hash identification reads the format (length, prefix, character set) and tells you whether you're looking at MD5, SHA-256, bcrypt, Argon2, NTLM, or something more exotic.
Try it: Hash Identifier β detect hash algorithm from format. Useful in incident response, breach investigation, and migrating off legacy systems where the original algorithm choice has been lost.
Generating cryptographic key pairs. Password hashing covers user authentication. Many systems also need asymmetric keys for signing JWTs, encrypting sensitive fields beyond password hashing, or service-to-service authentication. RSA, Elliptic Curve, and Ed25519 keys each have different use cases.
Try it: Key Pair Generator β generate RSA (2048/4096), EC (P-256/P-384), or Ed25519 key pairs for signing, encryption, and service auth.
Setup checklist
If you're implementing modern password hashing from scratch, work through these steps in order. They map to the HowTo schema attached to this article, so you can also follow them via Google's HowTo-rendered display if it picks up the markup.
-
Choose your algorithm based on the decision matrix: Argon2id for new apps, bcrypt for broad compatibility, PBKDF2-SHA256 for FIPS compliance.
-
Benchmark parameters on your production hardware. Target 250-400ms per hash. For Argon2id start at m=64 MB, t=3, p=1. For bcrypt start at cost=12. Adjust until you hit the target time.
-
Use a unique per-user salt β 16 bytes cryptographic random per user, never hardcoded, never reused. Modern libraries do this automatically; verify yours does.
-
Store the full encoded hash β algorithm identifier, version, parameters, salt, and hash all in a single VARCHAR(255) column. The self-describing format is what makes future migration possible.
-
Implement constant-time hash comparison β use the library's built-in
verify()function. Never compare hashes with==or.equals()β both leak timing information. -
Add a strong password policy at registration β minimum 12 characters, blocked common-password list, optional zxcvbn-style strength check. Slow hashing can't fix
password123. -
Add automatic rehashing on successful login β if the stored hash uses old parameters, rehash with current parameters and update the database. This rotates parameters gradually.
-
Build multi-algorithm dispatch logic β structure
verify()so it dispatches based on the stored hash's prefix. Even if you only use one algorithm today, you're building the foundation for any future migration. -
Log verification metrics but never the hash β track auth latency, failure rates, and rehash events. Never log passwords, hashes, or hash fragments.
-
Plan parameter rotation every 12-24 months. Hardware gets faster. What takes 300ms today will take 150ms in 18 months. Review and increase as part of regular security maintenance.
Frequently Asked Questions
Which password hashing algorithm should I use in 2026? Argon2id is the OWASP 2024+ recommendation and the right default for new applications. Use bcrypt only if you need broad library support or are already on bcrypt. Use PBKDF2-SHA256 only for FIPS 140-2 compliance. Avoid scrypt for new projects (Argon2 won the same memory-hard problem). Never use MD5, SHA-1, SHA-256, or SHA-512 directly for passwords.
Is bcrypt still secure in 2026? Yes β bcrypt at cost 12 or higher remains cryptographically sound. OWASP's 2024 cheat sheet lists it as acceptable. The reason Argon2 is recommended over bcrypt isn't a bcrypt weakness; it's that Argon2 is memory-hard and has better-tunable parameters.
What cost factor should I use for bcrypt in 2026? Minimum 12. Many production apps use 13 or 14. The factor is exponential. Reassess every 2-3 years and increase as hardware speeds up.
Why is Argon2id better than Argon2i and Argon2d? Argon2d optimizes against GPU brute force but is vulnerable to side-channel timing attacks. Argon2i resists timing attacks but is weaker against GPU brute force. Argon2id is a hybrid that runs i for the first iteration and d for the rest. RFC 9106 and OWASP both recommend id for password hashing.
Should I migrate from bcrypt to Argon2id? Not urgently if you're already at cost factor 12+. Bcrypt remains safe. Migrate when you're rewriting auth anyway, when compliance requires Argon2, or when your bcrypt cost is below 10.
What is a pepper and do I need one? A global secret added to passwords before hashing, stored outside the database. Protects against database-only leaks. Adds operational complexity (pepper rotation, secret management). Use only for high-value applications where defense-in-depth justifies the complexity.
How do I store password hashes in the database? Single VARCHAR(255) column with the full encoded output β algorithm identifier + parameters + salt + hash. Self-describing format lets you change algorithms without schema changes.
Can password hashing protect against weak passwords? No. Hashing buys time during a breach but can't make password123 meaningfully harder to crack. The complete model is strong password policy + good hashing + optional pepper + breach monitoring + account lockout.
Closing β what to do this week
Three takeaways, ranked by impact:
-
Use Argon2id for new applications. m=64 MB, t=3, p=1. Argon2id is the OWASP 2024 recommended default and the right answer for any greenfield project in 2026.
-
Target 250-400ms per hash on your production hardware. Benchmark, don't guess. The right parameter values depend on your CPU, your memory, and your latency budget β not on a number copied from a blog post.
-
Build migration-ready code from day one. Prefix-based dispatch on hash verification. Automatic rehash on successful login. Self-describing hash storage. These three patterns together mean you can swap algorithms in the future without forcing user password resets.
If you also handle email at scale, the equivalent guide for the email-authentication side of your stack is the Email Deliverability Guide 2026 β same practitioner focus, walking through SPF, DKIM, DMARC, and BIMI setup with working DNS examples.
The full set of tools referenced in this guide, in case you want them bookmarked: