# Argon2id vs bcrypt vs scrypt: Which Is Best?

# 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).

```js
// Node.js
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);  // cost factor 12
const ok = await bcrypt.compare(password, hash);
```

```python
# Python
import bcrypt
hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
ok = bcrypt.checkpw(password.encode(), hash)
```

```rust
// 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.

```js
// 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
# 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).

```js
// 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
# 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
// 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)

```js
// 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.

```js
// 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.

```sql
-- 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:

```js
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](/blog/xchacha20-poly1305-encryption) | [→ Why VaultKeepR is zero-knowledge](/blog/what-is-zero-knowledge-encryption)

## 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](/support) — we do security reviews for open-source projects.*

*Want to see how strong your own passwords are? Try the [password entropy checker](/password-strength-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.
