DEV Community

Geoffrey Kim
Geoffrey Kim

Posted on • Edited on

Finding the Optimal Bcrypt Rounds for Your Production Environment

When it comes to securely storing user passwords, using a strong hashing algorithm like bcrypt is essential. Bcrypt's adaptive work factor is its key security feature, allowing you to adjust computational cost as hardware capabilities evolve. The "rounds" parameter (technically the log2 of the iteration count) determines this cost. Selecting appropriate rounds is critical for balancing security and performance in production environments.

This guide presents a comprehensive approach to benchmarking and selecting optimal bcrypt configurations based on empirical testing, statistical analysis, and security best practices.

Understanding the Security Context

Before diving into benchmarking, it's important to understand what we're defending against:

  • Modern Attack Capabilities: As of 2025, dedicated password cracking hardware can test billions of bcrypt hashes per second at low rounds.
  • Minimum Security Threshold: OWASP and NIST recommend configurations that would require months of dedicated resources to crack a single password.
  • Threat Model Alignment: Higher-value applications require stronger protections. Financial services typically use higher rounds than content sites.

Step 1: Setting Up the Environment

To get started, install Node.js and the bcrypt library:

mkdir bcrypt-benchmark
cd bcrypt-benchmark
npm init -y
npm install bcrypt
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating a Comprehensive Benchmarking Suite

Create a file named bcrypt-benchmark.js with the following improved code:

const bcrypt = require('bcrypt');
const os = require('os');

// Configuration
const ROUNDS_TO_TEST = Array.from({ length: 12 }, (_, i) => i + 8); // Test rounds 8-19
const TEST_ITERATIONS = 5; // Run each test multiple times for statistical reliability
const PASSWORD_SAMPLES = [
  { type: 'simple', value: 'password123' },
  { type: 'complex', value: 'P@$$w0rd!2*3&4%5#' },
  { type: 'long', value: 'this is a much longer passphrase with multiple words' }
];
const CONCURRENCY_LEVELS = [1, 5, 10, 20]; // Different levels of concurrent hashing operations

// System information
console.log(`\nSystem Information:`);
console.log(`CPU: ${os.cpus()[0].model}`);
console.log(`Cores: ${os.cpus().length}`);
console.log(`Total Memory: ${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`);
console.log(`Platform: ${os.platform()} ${os.release()}`);
console.log(`Node.js Version: ${process.version}\n`);

// Helper function to measure memory usage
const getMemoryUsage = () => {
  const used = process.memoryUsage();
  return {
    rss: Math.round(used.rss / 1024 / 1024),
    heapTotal: Math.round(used.heapTotal / 1024 / 1024),
    heapUsed: Math.round(used.heapUsed / 1024 / 1024)
  };
};

// Test individual rounds with a specific password
const testRounds = async (rounds, password) => {
  const memBefore = getMemoryUsage();
  const startTime = process.hrtime.bigint();
  const hash = await bcrypt.hash(password, rounds);
  const endTime = process.hrtime.bigint();
  const memAfter = getMemoryUsage();

  // Convert from nanoseconds to milliseconds
  const duration = Number(endTime - startTime) / 1000000;

  // Verify the hash works correctly
  const isValid = await bcrypt.compare(password, hash);

  return {
    rounds,
    duration,
    memoryDelta: memAfter.heapUsed - memBefore.heapUsed,
    hashLength: hash.length,
    verified: isValid
  };
};

// Test concurrency by running multiple hashing operations simultaneously
const testConcurrency = async (rounds, password, concurrencyLevel) => {
  console.log(`\nTesting ${concurrencyLevel} concurrent operations with rounds=${rounds}...`);
  const startTime = process.hrtime.bigint();

  // Create an array of promises, each hashing the password
  const promises = Array(concurrencyLevel).fill().map(() => bcrypt.hash(password, rounds));

  // Wait for all hashing operations to complete
  await Promise.all(promises);

  const endTime = process.hrtime.bigint();
  const totalDuration = Number(endTime - startTime) / 1000000;

  return {
    rounds,
    concurrencyLevel,
    totalDuration,
    avgDuration: totalDuration / concurrencyLevel
  };
};

// Run the complete benchmark suite
const runBenchmark = async () => {
  console.log('Starting bcrypt benchmarking suite...\n');

  // 1. Single-operation benchmarks with different passwords
  for (const { type, value } of PASSWORD_SAMPLES) {
    console.log(`\n== Testing with ${type} password (${value.length} characters) ==`);

    for (const rounds of ROUNDS_TO_TEST) {
      // Run multiple iterations for statistical reliability
      const results = [];
      for (let i = 0; i < TEST_ITERATIONS; i++) {
        results.push(await testRounds(rounds, value));
      }

      // Calculate statistics
      const durations = results.map(r => r.duration);
      const avgDuration = durations.reduce((sum, d) => sum + d, 0) / durations.length;
      const minDuration = Math.min(...durations);
      const maxDuration = Math.max(...durations);
      const stdDev = Math.sqrt(
        durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) / durations.length
      );

      // Display results
      console.log(`Rounds: ${rounds}, Avg: ${avgDuration.toFixed(2)}ms, Min: ${minDuration.toFixed(2)}ms, Max: ${maxDuration.toFixed(2)}ms, StdDev: ${stdDev.toFixed(2)}ms, Memory: +${results[0].memoryDelta}MB`);
    }
  }

  // 2. Concurrency benchmarks (using only medium complexity password)
  const mediumPassword = PASSWORD_SAMPLES[1].value;

  // Select rounds in the middle of our range for concurrency testing
  const concurrencyRounds = [10, 12, 14];

  for (const rounds of concurrencyRounds) {
    for (const concurrencyLevel of CONCURRENCY_LEVELS) {
      const result = await testConcurrency(rounds, mediumPassword, concurrencyLevel);
      console.log(`Rounds: ${rounds}, Concurrent: ${concurrencyLevel}, Total: ${result.totalDuration.toFixed(2)}ms, Avg: ${result.avgDuration.toFixed(2)}ms`);
    }
  }

  console.log('\n== Benchmark Summary ==');
  console.log('Based on these benchmarks, you can select rounds that balance security and performance.');
  console.log(`
Security Level Guidelines:
- Minimum (8-10 rounds): ~${durations[8-8].toFixed()}ms per hash. Suitable only for low-risk applications.
- Standard (11-13 rounds): ~${durations[12-8].toFixed()}ms per hash. Good for most web applications.
- High (14-16 rounds): ~${durations[15-8].toFixed()}ms per hash. Recommended for financial or sensitive applications.
- Very High (17+ rounds): ${durations[17-8] ? '~'+durations[17-8].toFixed()+'ms' : '500ms+'} per hash. For extremely sensitive data.
  `);

  // Calculate optimal rounds based on the target range (250-500ms)
  const TARGET_MIN = 250;
  const TARGET_MAX = 500;

  let optimalRounds = ROUNDS_TO_TEST[0];
  let closestDuration = 0;

  for (let i = 0; i < ROUNDS_TO_TEST.length; i++) {
    const rounds = ROUNDS_TO_TEST[i];
    const duration = durations[i];

    if (duration >= TARGET_MIN && duration <= TARGET_MAX) {
      optimalRounds = rounds;
      closestDuration = duration;
      break;
    } else if (duration < TARGET_MIN) {
      optimalRounds = rounds;
      closestDuration = duration;
    }
  }

  console.log(`Recommended starting point: ${optimalRounds} rounds (~${closestDuration.toFixed()}ms per hash)`);
  console.log('Adjust based on your specific security requirements, user experience trade-offs, and server capacity.');
};

// Capture all durations for summary
const durations = [];

// Run the benchmark suite
runBenchmark().catch(err => console.error('Benchmark failed:', err));
Enter fullscreen mode Exit fullscreen mode

Step 3: Running the Benchmarking Suite

Execute the benchmarking script by running:

node bcrypt-benchmark.js
Enter fullscreen mode Exit fullscreen mode

The script will perform multiple tests, providing statistically significant results including:

  1. Per-round metrics: Average, minimum, maximum, and standard deviation for hash times
  2. Password type impact: How different password types affect hashing time
  3. Concurrency analysis: Performance under different levels of concurrent authentication requests
  4. Memory usage: Memory requirements for each configuration

Step 4: Interpreting Results and Selecting Optimal Rounds

When selecting the optimal rounds, consider these factors:

Security Requirements Mapping

Security Level Recommended Rounds (2025) Time Range Suitable For
Minimum 8-10 < 100ms Development, low-value accounts
Standard 11-13 100-250ms Most web applications
High 14-16 250-500ms Financial applications, admin accounts
Very High 17+ > 500ms Cryptocurrency, highly sensitive data

System Load Considerations

  • Authentication Traffic: High-traffic applications may need to select lower rounds
  • User Experience: Login operations should typically complete within 1 second
  • Server Resources: Ensure your authentication service can handle peak loads
  • Rate Limiting: Implement proper rate limiting to prevent DoS attacks

Step 5: Implementation and Monitoring Strategy

After selecting your optimal rounds, implement the following best practices:

// Example implementation with selected rounds
const BCRYPT_ROUNDS = 12; // Adjust based on your benchmarks

async function hashPassword(password) {
  return await bcrypt.hash(password, BCRYPT_ROUNDS);
}

async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}
Enter fullscreen mode Exit fullscreen mode

Monitoring and Evolution Strategy

Establish a regular review cycle for your bcrypt configuration:

  1. Quarterly Review: Re-run benchmarks to validate current settings
  2. Performance Monitoring: Set up monitoring for authentication response times
  3. Security Updates: Increase rounds when:
    • Average authentication time drops below your target range
    • New hardware/attacks significantly reduce the security margin
    • At least annually to account for hardware improvements

Advanced Considerations

Password Upgrade Strategy

Implement a mechanism to upgrade password hashes when users log in:

async function verifyAndUpgradeIfNeeded(password, hash) {
  // Verify password with existing hash
  const isValid = await bcrypt.compare(password, hash);

  if (isValid) {
    // Check if we need to upgrade the hash
    const hashInfo = bcrypt.getRounds(hash);
    if (hashInfo < BCRYPT_ROUNDS) {
      // Create a new hash with the current rounds setting
      const newHash = await bcrypt.hash(password, BCRYPT_ROUNDS);
      // Store the new hash in your database
      await updateUserHash(userId, newHash);
    }
    return true;
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

Work Queue Implementation

For high-traffic applications, implement a work queue for password hashing:

// Using a worker thread pool for password operations
const workerpool = require('workerpool');
const pool = workerpool.pool('./password-worker.js');

async function hashPasswordAsync(password) {
  return await pool.exec('hashPassword', [password, BCRYPT_ROUNDS]);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Finding the optimal bcrypt rounds configuration requires balancing security requirements with performance implications. The comprehensive benchmarking approach outlined here provides empirical data to make informed decisions based on your specific hardware, application requirements, and security needs.

Remember that security is an evolving target. What's secure today may not be tomorrow as computational power increases. Regularly revisit your bcrypt configuration to ensure it continues to provide adequate protection against evolving threats.

Additional Resources:

Top comments (0)