Argon2id vs bcrypt vs scrypt — Which Password Hashing Function Should You Use in 2026?
Argon2id vs bcrypt vs scrypt — Which Password Hashing Function Should You Use in 2026?
If you're building anything that stores user passwords, you have to choose a password hashing function. The three realistic options in 2026 are Argon2id, bcrypt, and scrypt. Each has different security properties, performance characteristics, and threat models.
This guide compares them head-to-head, with benchmarks, OWASP recommendations, and copy-paste code examples in Node.js, Python, and Rust.
What Is a Password Hashing Function?
A password hashing function is a slow, salted, one-way cryptographic function designed specifically for passwords. It is:
- Slow on purpose (millions to billions of times slower than SHA-256) to make brute force expensive
- Salted to prevent rainbow table attacks
- One-way so the original password cannot be recovered
General-purpose hash functions (MD5, SHA-256) are not suitable for passwords — they are designed to be fast, which is the opposite of what you want.
The Three Contenders
bcrypt (1999)
Designed by Niels Provos and David Mazières. Based on the Blowfish cipher. CPU-hard by design (uses iterative key schedule).
// Node.js
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12); // cost factor 12
const ok = await bcrypt.compare(password, hash);
# Python
import bcrypt
hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
ok = bcrypt.checkpw(password.encode(), hash)
// Rust
use bcrypt::{hash, verify, DEFAULT_COST};
let hash = hash(password, DEFAULT_COST)?;
let ok = verify(password, &hash)?;
scrypt (2009)
Designed by Colin Percival. Memory-hard — uses a configurable amount of RAM to make GPU/ASIC attacks expensive. Powers Tarsnap and many crypto wallets.
// Node.js (built-in)
const crypto = require('crypto');
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1 });
# Python
import hashlib
salt = os.urandom(16)
hash = hashlib.scrypt(password.encode(), salt=salt, n=16384, r=8, p=1, dklen=64)
Argon2id (2015)
Winner of the Password Hashing Competition. Three variants: Argon2d (data-dependent, fastest), Argon2i (data-independent, side-channel resistant), and Argon2id (hybrid — the recommended variant).
// Node.js
const argon2 = require('argon2');
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MiB
timeCost: 3,
parallelism: 4,
});
const ok = await argon2.verify(hash, password);
# Python
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hash = ph.hash(password)
ok = ph.verify(hash, password)
// Rust
use argon2::{Argon2, PasswordHasher, password_hash::{SaltString, rand_core::OsRng}};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
Technical Comparison
| Property | bcrypt | scrypt | Argon2id |
|---|---|---|---|
| Year | 1999 | 2009 | 2015 |
| Memory-hard | No | Yes | Yes |
| Parallelism tunable | No | Yes (blockmix p) | Yes (p param) |
| Iterations tunable | Yes (cost) | Yes (N) | Yes (t param) |
| Side-channel resistant | Partial | Yes (configurable) | Yes (Argon2i mode) |
| Max password length | 72 bytes (then silently truncated) | Unlimited | Unlimited |
| Standardized | de facto | RFC 7914 | RFC 9106 |
| OWASP recommended (2026) | Acceptable fallback | Acceptable | Preferred |
| GPU attack cost | Low (cheap to attack) | High | High |
| ASIC attack cost | Low | Medium-High | High |
Performance Benchmarks (2026, single core, modern x86)
| Algorithm | Memory | Time | Hashes/sec on RTX 4090 |
|---|---|---|---|
| bcrypt (cost 12) | 4 KB | ~250 ms | ~4,000 |
| scrypt (N=2^15, r=8, p=1) | 32 MB | ~200 ms | ~50 |
| Argon2id (64MB, t=3, p=4) | 256 MB | ~300 ms | ~5 |
| Argon2id (19MB, t=2, p=1) | 19 MB | ~50 ms | ~200 |
| SHA-256 (for comparison) | 0 | < 1 µs | 10 billion+ |
The key insight: Argon2id is 3 orders of magnitude harder to attack on GPU than bcrypt, because of its memory-hardness.
Threat Models
Why bcrypt is no longer the best choice
bcrypt's 72-byte password truncation is a real problem. If your user picks a passphrase like "correct horse battery staple", bcrypt only hashes the first 72 bytes (about 60 characters). The rest is ignored. Combined with bcrypt's tiny memory footprint (4 KB), an attacker with a stack of GPUs can crack bcrypt hashes at thousands of guesses per second per GPU.
Why scrypt is good but rarely used
scrypt was the first practical memory-hard KDF and is excellent in theory. But:
- The
Nparameter is the iteration count, but ther(block size) andp(parallelism) parameters are less intuitive - Few production-quality libraries have a stable, audited implementation
- It's harder to configure safely than Argon2id
Why Argon2id wins
- OWASP-recommended since 2017
- Three orthogonal parameters (memory, time, parallelism) that can be tuned independently
- Hybrid mode (id) combines Argon2d's GPU resistance with Argon2i's side-channel resistance
- Standardized as RFC 9106
- Memory-hard by construction, not as a configuration option
OWASP Password Storage Cheat Sheet (2026)
| Setting | Memory (KiB) | Iterations | Parallelism | Use case |
|---|---|---|---|---|
| Minimum | 19,456 (19 MiB) | 2 | 1 | Interactive login (under 1 second) |
| Recommended | 65,536 (64 MiB) | 3 | 4 | Standard web application |
| High security | 262,144 (256 MiB) | 4 | 8 | Sensitive data, admin accounts |
For bcrypt, OWASP recommends a minimum cost factor of 12 (rising to 13+ over time).
Common Mistakes
1. Using a fast hash (MD5, SHA-1, SHA-256)
// NEVER DO THIS
const hash = crypto.createHash('sha256').update(password).digest('hex');
This is hashing at gigabytes per second. An attacker with a single GPU will crack your entire user database in days.
2. Not salting
Without a unique random salt per user, an attacker uses rainbow tables — pre-computed hashes for billions of common passwords.
// Wrong: same salt for everyone
const hash = bcrypt.hash(password, '$2b$10$commonSaltForAllUsers');
// Right: random salt per user (handled automatically by bcrypt/argon2 libs)
const hash = bcrypt.hash(password, 12);
3. Re-hashing on every login
Some developers re-hash the password on every successful login, thinking it adds security. It doesn't — it just wastes CPU and creates race conditions in multi-threaded servers. Hash once, store the parameters, verify on login.
4. Storing the hash in a field that's too short
If you use bcrypt, the hash is exactly 60 characters. If you use Argon2id, it's 80-100+ characters depending on parameters. Make sure your database column is large enough.
-- PostgreSQL
ALTER TABLE users ADD COLUMN password_hash TEXT; -- not VARCHAR(50)
5. Using outdated cost factors
bcrypt cost 10 was reasonable in 2010. In 2026, you need cost 12 minimum, 13+ preferred. Argon2id parameters should be revisited every 2 years as hardware gets faster.
Migration Strategy: bcrypt → Argon2id
You don't need to migrate all users at once. Use a "lazy migration" approach:
async function verifyAndMigrate(password, storedHash) {
if (await argon2.verify(storedHash, password)) {
// Check if it was an old bcrypt hash
if (storedHash.startsWith('$2b$') || storedHash.startsWith('$2a$')) {
const newHash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4,
});
await db.updateUserHash(userId, newHash);
}
return true;
}
return false;
}
The next time a user logs in successfully, their password is silently re-hashed with Argon2id. After 90 days, 90%+ of active users are migrated.
How VaultKeepR Uses Argon2id
VaultKeepR uses Argon2id as the key derivation function for your master vault key. The parameters are tuned for modern hardware (64 MiB memory, 3 iterations, 4-way parallelism) and re-evaluated every release. Combined with XChaCha20-Poly1305 for vault encryption, this gives you state-of-the-art security without the legacy compromises of bcrypt.
→ Read our detailed crypto stack | → Why VaultKeepR is zero-knowledge
Final Recommendation
| Use case | Recommendation |
|---|---|
| New application in 2026 | Argon2id with OWASP-recommended parameters |
| Existing bcrypt system | Keep bcrypt for now, migrate lazily to Argon2id |
| Crypto wallet / hardware key derivation | Argon2id with high memory (256 MiB+) |
| IoT / embedded device | scrypt with conservative parameters |
| Maximum performance (you know what you're doing) | Argon2id, tuned for your specific hardware |
Bottom line: In 2026, if you're starting a new project, use Argon2id. There is no good reason to choose bcrypt for new code, and even scrypt is showing its age. Argon2id is the modern, OWASP-recommended, future-proof choice.
Need help auditing your password storage implementation? Get in touch — we do security reviews for open-source projects.
Ready to take control of your passwords?
VaultKeepR is the first decentralized password manager. Zero-knowledge. Wallet-native. Yours.
Try VaultKeepR →