Skip to content

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

@titanism

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions