EncryptCodecencryptcodec
Blog/Security
SecurityMarch 18, 2026 · 7 min read

Password Hashing in 2025 — bcrypt vs Argon2 vs scrypt

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.

const argon2 = require('argon2');

// Hash a password
async function hashPassword(password) {
return await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 19456,  // 19MB in KiB
  timeCost: 2,
  parallelism: 1,
});
}

// Verify a password
async function verifyPassword(hash, password) {
return await argon2.verify(hash, password);
}

// Usage
const hash = await hashPassword('hunter2');
const isValid = await verifyPassword(hash, 'hunter2'); // true

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:

  1. User logs in — you receive their plaintext password
  2. Verify against the existing bcrypt hash
  3. If it passes, immediately hash the password with Argon2id and overwrite the stored hash
  4. 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.

ALGORITHM PARAMETER EXPLORER
PRESET
PARAMETERS
Memory64 MB
Parallelism1 threads
HASH TIME (estimated)
210ms✓ In target range
0ms100–300ms target1.5s
SECURITY PROPERTIES
GPU resistance
98%
Side-channel safe
95%
Memory hardness
95%

Best choice for new systems. Memory-hard, side-channel resistant.

GPU attack cost
~64 parallel hashes/GPU
High memory forces GPU serialization

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.

Share this post

Check password strength in your browser

Free, browser-based — no signup required.

Frequently Asked Questions

Related posts