-
-
Notifications
You must be signed in to change notification settings - Fork 40
Description
EDIT: See comment at #1041 (comment) 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'}`)