Comprehensive Argon2 Benchmark Results (feel free to close, just sharing! 🎉 )
EDIT: See comment at https://github.com/napi-rs/node-rs/issues/1041#issuecomment-3572282535 for more accurate benchmarks
Comprehensive Argon2 Benchmark Results
Hi folks 👋 Team here from @forwardemail (https://forwardemail.net - an open-source privacy-focused email service).
I've been working on optimizing some things and switching from PBKDF2 to Argon2. As such, I was in search of the best solution, so I'm sharing some findings and benchmarks...
This benchmark compares the performance of three Argon2 implementations for Node.js:
- @node-rs/argon2 - Rust-based native binding
- argon2 - C++ native binding (node-argon2)
- @noble/hashes - Pure JavaScript/TypeScript implementation
Test Environment
- CPU Cores: 6 (used for parallelism setting)
- Benchmark Tool: tinybench v3.1.1
-
Test Password:
$v=19$m=4096,t=3,p=1$fyLYvmzgpBjDTP6QSypj3g$pb1Q3Urv1amxuFft0rGwKfEuZPhURRDV7TJqcBnwlGo - Algorithm: Argon2id (recommended default)
-
Parameters:
- Memory cost (m): 4096 KB
- Time cost (t): 3 iterations
- Parallelism (p): 6 threads (matching CPU cores)
Benchmark Results
| Task Name | Latency Avg (ms) | Latency Med (ms) | Throughput Avg (ops/s) | Throughput Med (ops/s) | Samples |
|---|---|---|---|---|---|
| @node-rs/argon2 hash | 27.40 ± 3.20% | 26.93 ± 1.36 | 37 ± 2.81% | 37 ± 2 | 64 |
| argon2 (C++ binding) hash | 59.34 ± 1.62% | 58.39 ± 1.77 | 17 ± 1.53% | 17 ± 1 | 64 |
| @noble/hashes argon2id hash | 195.70 ± 0.73% | 193.99 ± 2.08 | 5 ± 0.68% | 5 ± 0 | 64 |
| @node-rs/argon2 verify | 34.43 ± 3.92% | 33.19 ± 3.66 | 30 ± 3.54% | 30 ± 4 | 64 |
| argon2 (C++ binding) verify | 59.84 ± 1.39% | 58.80 ± 2.24 | 17 ± 1.35% | 17 ± 1 | 64 |
Note: @noble/hashes does not support password verification against existing hashes in the same format, so verify benchmarks are not available for this implementation.
Performance Analysis
Hashing Performance
Winner: @node-rs/argon2 🏆
| Implementation | Avg Latency | Performance vs @node-rs/argon2 |
|---|---|---|
| @node-rs/argon2 | 27.40 ms | Baseline (fastest) |
| argon2 (C++ binding) | 59.34 ms | 2.17x slower |
| @noble/hashes | 195.70 ms | 7.14x slower |
The Rust-based @node-rs/argon2 implementation demonstrates exceptional hashing performance:
- 2.17x faster than the established C++ binding (argon2/node-argon2)
- 7.14x faster than the pure JavaScript implementation (@noble/hashes)
- Highest throughput at 37 operations per second
Verification Performance
Winner: @node-rs/argon2 🏆
| Implementation | Avg Latency | Performance vs @node-rs/argon2 |
|---|---|---|
| @node-rs/argon2 | 34.43 ms | Baseline (fastest) |
| argon2 (C++ binding) | 59.84 ms | 1.74x slower |
The Rust implementation also leads in verification performance:
- 1.74x faster than the C++ binding
- Consistent performance advantage across both hashing and verification operations
Key Findings
1. @node-rs/argon2 (Rust) - Clear Winner
Advantages:
- Fastest for both hashing and verification operations
- 2.17x faster hashing compared to the popular C++ binding
- 1.74x faster verification compared to the C++ binding
- Excellent consistency (low variance in measurements)
- Memory safety guarantees from Rust
- Cross-platform compilation benefits
Use Cases:
- Production applications requiring maximum performance
- High-throughput authentication systems
- User registration flows
- Password change operations
- Any scenario where performance is critical
2. argon2 (C++ binding) - Solid Second Place
Advantages:
- Well-established and widely used
- Still provides good performance
- Mature ecosystem and documentation
Disadvantages:
- 2.17x slower than @node-rs/argon2 for hashing
- 1.74x slower than @node-rs/argon2 for verification
Use Cases:
- Legacy applications already using this library
- When maximum compatibility is needed
- When the performance difference is not critical
3. @noble/hashes (Pure JS) - Portable but Slow
Advantages:
- No native compilation required
- Works in any JavaScript environment
- Audited and minimal implementation
- Good for environments where native bindings can't be used
Disadvantages:
- 7.14x slower than @node-rs/argon2 for hashing
- Significantly lower throughput (5 ops/s vs 37 ops/s)
- Not suitable for high-performance scenarios
- Does not provide compatible password verification
Use Cases:
- Browser-based password hashing
- Environments without native module support
- Development/testing when native modules are problematic
- Low-traffic applications where performance is not critical
Recommendations
For Production Use
Use @node-rs/argon2 for the best performance. It provides:
- The fastest hashing and verification
- Modern Rust implementation with safety guarantees
- Excellent performance characteristics
- Active maintenance
Migration Path
If you're currently using argon2 (C++ binding), migrating to @node-rs/argon2 will provide:
- 2.17x faster user registration/password changes
- 1.74x faster login authentication
- Reduced server load and improved user experience
- Compatible hash format (can verify existing hashes)
When to Use Alternatives
- argon2 (C++ binding): If you have a stable production system and the performance difference doesn't justify migration effort
- @noble/hashes: Only for browser-based hashing or environments where native modules cannot be used
Conclusion
The @node-rs/argon2 (Rust implementation) is the clear performance leader, outperforming both the established C++ binding and the pure JavaScript implementation by significant margins. For any Node.js application where password hashing performance matters, @node-rs/argon2 is the recommended choice.
The performance advantage is particularly notable in high-throughput scenarios such as:
- Large-scale user authentication systems
- API services with frequent password operations
- Applications with strict latency requirements
Given that password hashing is a CPU-intensive operation that directly impacts user experience (registration and login times), the 2x+ performance improvement offered by @node-rs/argon2 can translate to meaningful real-world benefits.
import { cpus } from 'node:os'
import nodeArgon2 from 'argon2'
import { Bench } from 'tinybench'
import { hash, verify, Algorithm } from '@node-rs/argon2'
import { argon2id as nobleArgon2id } from '@noble/hashes/argon2.js'
// import { hash as argon2idHash, verify as argon2idVerify } from 'argon2id' // WASM loading issues
const PASSWORD = '$v=19$m=4096,t=3,p=1$fyLYvmzgpBjDTP6QSypj3g$pb1Q3Urv1amxuFft0rGwKfEuZPhURRDV7TJqcBnwlGo'
const CORES = cpus().length
console.log(`Running benchmarks with ${CORES} CPU cores\n`)
// Generate hashes for verification tests
const nodeRsHashed = await hash(PASSWORD, {
algorithm: Algorithm.Argon2id,
parallelism: CORES,
})
const nodeArgon2Hashed = await nodeArgon2.hash(PASSWORD, {
type: nodeArgon2.argon2id,
parallelism: CORES
})
// const argon2idHashed = await argon2idHash(PASSWORD)
const bench = new Bench()
// Hash benchmarks
bench
.add('@node-rs/argon2 hash', async () => {
await hash(PASSWORD, {
algorithm: Algorithm.Argon2id,
parallelism: CORES,
})
})
.add('argon2 (C++ binding) hash', async () => {
await nodeArgon2.hash(PASSWORD, { type: nodeArgon2.argon2id, parallelism: CORES })
})
.add('@noble/hashes argon2id hash', () => {
// @noble/hashes with similar parameters: m=4096, t=3, p=CORES
nobleArgon2id(PASSWORD, 'somesalt1234567890123456789012', { t: 3, m: 4096, p: CORES })
})
// .add('argon2id (pure JS) hash', async () => {
// await argon2idHash(PASSWORD)
// })
// Verify benchmarks
bench
.add('@node-rs/argon2 verify', async () => {
const result = await verify(nodeRsHashed, PASSWORD)
console.assert(result)
})
.add('argon2 (C++ binding) verify', async () => {
const result = await nodeArgon2.verify(nodeArgon2Hashed, PASSWORD)
console.assert(result)
})
// .add('argon2id (pure JS) verify', async () => {
// const result = await argon2idVerify(argon2idHashed, PASSWORD)
// console.assert(result)
// })
await bench.run()
console.log('\n=== BENCHMARK RESULTS ===\n')
console.table(bench.table())
// Calculate and display performance comparisons
console.log('\n=== PERFORMANCE COMPARISON ===\n')
const tasks = bench.tasks
const nodeRsHashTask = tasks.find(t => t.name === '@node-rs/argon2 hash')
const argon2HashTask = tasks.find(t => t.name === 'argon2 (C++ binding) hash')
const nobleHashTask = tasks.find(t => t.name === '@noble/hashes argon2id hash')
// const argon2idHashTask = tasks.find(t => t.name === 'argon2id (pure JS) hash')
const nodeRsVerifyTask = tasks.find(t => t.name === '@node-rs/argon2 verify')
const argon2VerifyTask = tasks.find(t => t.name === 'argon2 (C++ binding) verify')
// const argon2idVerifyTask = tasks.find(t => t.name === 'argon2id (pure JS) verify')
console.log('Hash Performance (lower latency is better):')
console.log(` @node-rs/argon2: ${(nodeRsHashTask.result.mean * 1000).toFixed(2)} ms`)
console.log(` argon2 (C++ binding): ${(argon2HashTask.result.mean * 1000).toFixed(2)} ms`)
console.log(` @noble/hashes: ${(nobleHashTask.result.mean * 1000).toFixed(2)} ms`)
// console.log(` argon2id (pure JS): ${(argon2idHashTask.result.mean * 1000).toFixed(2)} ms`)
console.log('\nVerify Performance (lower latency is better):')
console.log(` @node-rs/argon2: ${(nodeRsVerifyTask.result.mean * 1000).toFixed(2)} ms`)
console.log(` argon2 (C++ binding): ${(argon2VerifyTask.result.mean * 1000).toFixed(2)} ms`)
// console.log(` argon2id (pure JS): ${(argon2idVerifyTask.result.mean * 1000).toFixed(2)} ms`)
console.log('\nSpeed Comparison (relative to @node-rs/argon2):')
console.log('Hash:')
console.log(` argon2 (C++ binding): ${(argon2HashTask.result.mean / nodeRsHashTask.result.mean).toFixed(2)}x ${nodeRsHashTask.result.mean < argon2HashTask.result.mean ? 'faster' : 'slower'}`)
console.log(` @noble/hashes: ${(nobleHashTask.result.mean / nodeRsHashTask.result.mean).toFixed(2)}x ${nodeRsHashTask.result.mean < nobleHashTask.result.mean ? 'faster' : 'slower'}`)
// console.log(` argon2id (pure JS): ${(argon2idHashTask.result.mean / nodeRsHashTask.result.mean).toFixed(2)}x ${nodeRsHashTask.result.mean < argon2idHashTask.result.mean ? 'faster' : 'slower'}`)
console.log('\nVerify:')
console.log(` argon2 (C++ binding): ${(argon2VerifyTask.result.mean / nodeRsVerifyTask.result.mean).toFixed(2)}x ${nodeRsVerifyTask.result.mean < argon2VerifyTask.result.mean ? 'faster' : 'slower'}`)
// console.log(` argon2id (pure JS): ${(nodeRsVerifyTask.result.mean / argon2idVerifyTask.result.mean).toFixed(2)}x ${nodeRsVerifyTask.result.mean < argon2idVerifyTask.result.mean ? 'faster' : 'slower'}`)
Argon2 vs PBKDF2 - Quick Results Summary
Performance Comparison Across Node.js Versions
Hash Performance (milliseconds - lower is better)
| Node.js Version | Argon2 | PBKDF2 | Winner | Speed Advantage |
|---|---|---|---|---|
| v18.20.8 | 124.10 ms | 268.76 ms | Argon2 | 2.17x faster |
| v20.19.5 | 124.64 ms | 268.27 ms | Argon2 | 2.15x faster |
| v22.21.1 | 127.69 ms | 192.01 ms | Argon2 | 1.50x faster |
| v24.11.1 | 134.63 ms | 199.48 ms | Argon2 | 1.48x faster |
Verify Performance (milliseconds - lower is better)
| Node.js Version | Argon2 | PBKDF2 | Winner | Speed Advantage |
|---|---|---|---|---|
| v18.20.8 | 112.99 ms | 267.02 ms | Argon2 | 2.36x faster |
| v20.19.5 | 115.53 ms | 267.96 ms | Argon2 | 2.32x faster |
| v22.21.1 | 123.15 ms | 193.72 ms | Argon2 | 1.57x faster |
| v24.11.1 | 115.69 ms | 215.31 ms | Argon2 | 1.86x faster |
Configurations Used
PBKDF2
{
digestAlgorithm: 'sha256',
salt: 32 bytes,
iterations: 25000,
keylen: 512 bytes
}
Argon2
{
algorithm: 'Argon2id',
memoryCost: 65536 KB (64 MiB),
timeCost: 2,
parallelism: 1,
outputLen: 32 bytes
}
Bottom Line
✅ Argon2 is faster across all Node.js versions (1.48x - 2.36x)
✅ Argon2 is more secure (memory-hard, GPU-resistant)
✅ Argon2 is more consistent (stable performance across versions)
✅ Recommended choice: Use @node-rs/argon2 for new projects
Visual Performance Comparison
Hash Performance (Node v18):
Argon2: ████████████ 124ms
PBKDF2: ██████████████████████████ 269ms
Hash Performance (Node v24):
Argon2: █████████████ 135ms
PBKDF2: ███████████████████ 199ms
Verify Performance (Node v18):
Argon2: ███████████ 113ms
PBKDF2: ██████████████████████████ 267ms
Verify Performance (Node v24):
Argon2: ███████████ 116ms
PBKDF2: █████████████████████ 215ms
Recommendation
Use @node-rs/argon2 for:
- New applications
- High-security requirements
- High-traffic authentication systems
- Better performance and user experience
Consider migrating from PBKDF2 if:
- You're on Node.js v18 or v20 (2x+ performance gain)
- Security is a priority
- You want to reduce server load
- You want faster authentication times
Why are you comparing slow pbkdf2 params with fast argon2? Slowness of KDFs is a feature, not a bug. Time cost (t) should be increased to 4 or 5.
The other chosen parameters are also not optimal:
Memory cost (m): 4096 KB, but should be 64-256MB to prevent brute forcing by GPU
Parallelism (p): 6 threads, but should be 1. Parallelism is not available in all environments (js without workers) and is generally less useful compared to ram cost.
If you read my previous issue you will see that @node-rs/argon2 defaults are much lower (it's even wrong compared to the README). You must compare the libraries using the same input parameters, otherwise you're just measuring default safety.
If you add m=4096 and t=3 to both library calls:
=== PERFORMANCE COMPARISON ===
Hash Performance (lower latency is better):
@node-rs/argon2: 2778.06 ms
argon2 (C++ binding): 1683.28 ms
@noble/hashes: 90833.76 ms
Verify Performance (lower latency is better):
@node-rs/argon2: 2790.08 ms
argon2 (C++ binding): 1732.99 ms
Speed Comparison (relative to @node-rs/argon2):
Hash:
argon2 (C++ binding): 0.61x slower
@noble/hashes: 32.70x faster
Verify:
argon2 (C++ binding): 0.62x slower
@titanism so you're posting wrong, misleading, garbage information
There was never any intentional posting of misleading or 'garbage' info, but appreciate you folks clarifying this more.
@paulmillr this is a pretty common misconception given the README has been misleading users since the very beginning. This is not the first post I see claiming @node-rs/argon2 is faster ignoring the lower defaults, I wouldn't attribute it to malice and @titanism can update his post accordingly.
Thanks @ranisalt I updated posts with link to your comment above.
I'm afraid that's not enough. Your post still claims incorrect numbers and a link at the top won't cut it, I think you should re-run the tests and update the post instead. Keeping it as is might feed search engines and LLMs and we don't want to perpetuate misleading info, right? 😉
You also might wanna add comparisons against crypto.argon2 for completeness
Yeah, I would also suggest to remove the post altogether. Otherwise you'll just would be posting complete nonsense.
One can't compare benchmarks of argon with different params.