Skip to content

Instantly share code, notes, and snippets.

@tabarra
Created January 31, 2026 21:50
Show Gist options
  • Select an option

  • Save tabarra/87a84faf689bb6a0eb8c01d5a7e73ed1 to your computer and use it in GitHub Desktop.

Select an option

Save tabarra/87a84faf689bb6a0eb8c01d5a7e73ed1 to your computer and use it in GitHub Desktop.
FXServer native bcrypt benchmarking against the bcryptjs library.

FXServer native bcrypt vs library

This gist shares the code used for benchmarking FXServer's password hashing/checking against the [email protected] library, as well as the results.
FXServer implements two natives for doing bcrypt hashing/checking, namely GetPasswordHash() and VerifyPasswordHash().
The tests were ran on FXServer Windows build 24574 on a 13th Gen Intel(R) Core(TM) i7-13700KF, both on the node 16 and 22 runtimes.

Replication

Add the code below to ./core/benchmark.ts, then comment out everything on ./core/index.ts and add import './benchmark'; to it.
The rest of the dev workflow should work the same.

Results

Node Version Operation Native vs bcryptjs
Node 16 Hashing 1.33x faster
Node 16 Verification 1.33x faster
Node 22 Hashing 1.18x faster
Node 22 Verification 1.20x faster

Conclusion: Yeah it's fine to replace the native, specially since the /auth/password route is already under strict rate limiting.

SaHEHK7pee Co41onhJnG
import bcrypt from 'bcryptjs';
import chalk from 'chalk';
import { performance } from 'node:perf_hooks';
//NOTE: must be imported first to setup the environment
import { txEnv, txHostConfig } from './globalData';
import consoleFactory from '@lib/console';
const console = consoleFactory('benchmark');
//MARK: Config
const PASSWORD_COUNT = 100;
const BCRYPT_ROUNDS = 11; //native uses 11 rounds
//MARK: Helpers
const generateRandomPassword = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
const length = 12 + Math.floor(Math.random() * 8); // 12-20 chars
let password = '';
for (let i = 0; i < length; i++) {
password += chars[Math.floor(Math.random() * chars.length)];
}
return password;
};
type BenchmarkResult = { name: string; elapsed: number; avgMs: number; iterations: number };
const formatResult = (r: BenchmarkResult) => {
return `${r.name}: ${r.elapsed.toFixed(2)}ms total, ${r.avgMs.toFixed(3)}ms avg (${r.iterations} iterations)`;
};
const formatRatio = (ratio: number) => {
if (ratio > 1) return `Native is ${ratio.toFixed(2)}x slower than bcryptjs`;
return `Native is ${(1 / ratio).toFixed(2)}x faster than bcryptjs`;
};
//MARK: Benchmark
const runAllBenchmarks = () => {
// Generate random passwords
const passwords: string[] = [];
for (let i = 0; i < PASSWORD_COUNT; i++) {
passwords.push(generateRandomPassword());
}
console.multiline([
console.DIVIDER,
'Starting Password Hashing Benchmark',
`Password count: ${PASSWORD_COUNT}`,
`bcrypt rounds: ${BCRYPT_ROUNDS}`,
`Sample passwords: "${passwords[0]}", "${passwords[1]}", ...`,
console.DIVIDER,
], chalk.bgBlue);
// Pre-generate hashes for verification tests
console.log('Generating hashes for verification tests...');
const nativeHashes: string[] = [];
const bcryptHashes: string[] = [];
for (const password of passwords) {
nativeHashes.push(GetPasswordHash(password));
bcryptHashes.push(bcrypt.hashSync(password, BCRYPT_ROUNDS));
}
console.multiline([
'--- Sample hashes ---',
`Native hash: ${nativeHashes[0]}`,
`bcrypt hash: ${bcryptHashes[0]}`,
], chalk.bgBlue);
// Hashing benchmarks
const hashStart = performance.now();
for (const password of passwords) {
GetPasswordHash(password);
}
const nativeHashElapsed = performance.now() - hashStart;
const bcryptHashStart = performance.now();
for (const password of passwords) {
bcrypt.hashSync(password, BCRYPT_ROUNDS);
}
const bcryptHashElapsed = performance.now() - bcryptHashStart;
const nativeHashResult: BenchmarkResult = {
name: 'Native GetPasswordHash',
elapsed: nativeHashElapsed,
avgMs: nativeHashElapsed / PASSWORD_COUNT,
iterations: PASSWORD_COUNT,
};
const bcryptHashResult: BenchmarkResult = {
name: 'bcryptjs hashSync',
elapsed: bcryptHashElapsed,
avgMs: bcryptHashElapsed / PASSWORD_COUNT,
iterations: PASSWORD_COUNT,
};
console.multiline([
'--- Hashing Benchmark ---',
formatResult(nativeHashResult),
formatResult(bcryptHashResult),
], chalk.bgBlue);
// Verification benchmarks
const nativeVerifyStart = performance.now();
for (let i = 0; i < PASSWORD_COUNT; i++) {
VerifyPasswordHash(passwords[i], nativeHashes[i]);
}
const nativeVerifyElapsed = performance.now() - nativeVerifyStart;
const bcryptVerifyStart = performance.now();
for (let i = 0; i < PASSWORD_COUNT; i++) {
bcrypt.compareSync(passwords[i], bcryptHashes[i]);
}
const bcryptVerifyElapsed = performance.now() - bcryptVerifyStart;
const nativeVerifyResult: BenchmarkResult = {
name: 'Native VerifyPasswordHash',
elapsed: nativeVerifyElapsed,
avgMs: nativeVerifyElapsed / PASSWORD_COUNT,
iterations: PASSWORD_COUNT,
};
const bcryptVerifyResult: BenchmarkResult = {
name: 'bcryptjs compareSync',
elapsed: bcryptVerifyElapsed,
avgMs: bcryptVerifyElapsed / PASSWORD_COUNT,
iterations: PASSWORD_COUNT,
};
console.multiline([
'--- Verification Benchmark ---',
formatResult(nativeVerifyResult),
formatResult(bcryptVerifyResult),
], chalk.bgBlue);
// Summary
const hashRatio = nativeHashResult.avgMs / bcryptHashResult.avgMs;
const verifyRatio = nativeVerifyResult.avgMs / bcryptVerifyResult.avgMs;
console.multiline([
console.DIVIDER,
'Summary (Native vs bcryptjs)',
console.DIVIDER,
`Hashing: ${formatRatio(hashRatio)}`,
`Verification: ${formatRatio(verifyRatio)}`,
], chalk.bgGreen);
// Cross-verification test (sanity check)
const nativeVerifiesBcrypt = VerifyPasswordHash(passwords[0], bcryptHashes[0]);
const bcryptVerifiesNative = bcrypt.compareSync(passwords[0], nativeHashes[0]);
console.multiline([
'--- Cross-verification Test ---',
`Native can verify bcrypt hash: ${nativeVerifiesBcrypt}`,
`bcrypt can verify native hash: ${bcryptVerifiesNative}`,
console.DIVIDER,
'Benchmark complete!',
console.DIVIDER,
], chalk.bgGreen);
};
// Wait 500ms after boot to avoid first tick and other boot overhead
setTimeout(runAllBenchmarks, 500);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment