Skip to content

Comprehensive Argon2 Benchmark Results (feel free to close, just sharing! 🎉 ) #1041

Description

@titanism

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'}`)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions