Skip to main content

Command Palette

Search for a command to run...

Argon2id vs bcrypt vs scrypt: Which Is Best?

Updated
8 min readView as Markdown

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 N parameter is the iteration count, but the r (block size) and p (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

  1. OWASP-recommended since 2017
  2. Three orthogonal parameters (memory, time, parallelism) that can be tuned independently
  3. Hybrid mode (id) combines Argon2d's GPU resistance with Argon2i's side-channel resistance
  4. Standardized as RFC 9106
  5. 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.

Want to see how strong your own passwords are? Try the password entropy checker — it computes the same math client-side.

FAQ

Is Argon2id better than bcrypt?

Yes, in 2026 Argon2id is the OWASP-recommended choice for password hashing. It is memory-hard (resistant to GPU and ASIC attacks), tunable across three independent parameters, and won the Password Hashing Competition in 2015. bcrypt remains acceptable but is more vulnerable to specialized hardware.

Should I still use bcrypt in 2026?

bcrypt is still considered safe for existing systems, but new applications should use Argon2id. bcrypt's 72-byte password truncation and small memory footprint make it less future-proof against hardware advances.

How much memory should Argon2id use?

OWASP recommends at least 19 MiB of memory, 2 iterations, and 1 parallelism for interactive logins (under 1 second). For high-security applications, 64 MiB / 3 iterations / 4 parallelism is a common production setting.