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
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));
Step 3: Running the Benchmarking Suite
Execute the benchmarking script by running:
node bcrypt-benchmark.js
The script will perform multiple tests, providing statistically significant results including:
- Per-round metrics: Average, minimum, maximum, and standard deviation for hash times
- Password type impact: How different password types affect hashing time
- Concurrency analysis: Performance under different levels of concurrent authentication requests
- 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);
}
Monitoring and Evolution Strategy
Establish a regular review cycle for your bcrypt configuration:
- Quarterly Review: Re-run benchmarks to validate current settings
- Performance Monitoring: Set up monitoring for authentication response times
-
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;
}
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]);
}
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)