Password Hashing

Passwords are the one place where a slow hash is the goal. SHA-256 runs at roughly half a gigabyte a second here (Hashes and HMACs), so a leaked table of SHA-256 password hashes falls to a GPU in hours. A password hash is deliberately expensive, memory-hungry so a GPU's thousands of small cores cannot each hold a working set, and salted so two users with the same password get different hashes. bcrypt (1999) is tuned by one cost factor; scrypt ships with Node and takes N (CPU/memory cost, default 16384), r (block size, default 8) and p; Argon2id won the 2015 Password Hashing Competition and tunes memory, time and parallelism separately.

One password, three algorithmsJavaScript
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
import bcrypt from 'bcrypt';        // npm i bcrypt@6.0.0 argon2@0.45.1
import argon2 from 'argon2';
const scryptAsync = promisify(scrypt);
const PASSWORD = 'correct horse battery staple';
console.log('bcrypt  :', await bcrypt.hash(PASSWORD, 12));
const a2 = await argon2.hash(PASSWORD,
  { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 4 });
console.log('argon2id:', a2.slice(0, 70) + '...');
const salt = randomBytes(16);       // scrypt returns raw bytes: you design the record
const params = { N: 2 ** 16, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
const key = await scryptAsync(PASSWORD, salt, 64, params);
const again = await scryptAsync(PASSWORD, salt, 64, params);
console.log('scrypt verify:', timingSafeEqual(key, again));
console.log('bcrypt wrong :', await bcrypt.compare('wrong', await bcrypt.hash(PASSWORD, 10)));
Output
bcrypt  : $2b$12$I.7DtlSb29MHU..z2rRyfukmg7SM0XECFbIsHttvSMOv14QQrhpOm
argon2id: $argon2id$v=19$m=65536,p=4,t=3$69WW9i8V6rqj1hasESWDhg$HQMdIk8ToNr4rZQq...
scrypt verify: true
bcrypt wrong : false

bcrypt and argon2 return self-describing strings — cost parameters and salt travel with the hash, so compare/verify need nothing else and raising the cost later does not invalidate old rows. Node's scrypt returns raw bytes: store the parameters and salt yourself, then compare with timingSafeEqual.

Hashing one password, Node 25.8.0, Windows 11 x64, 5 runs each
Algorithm and parameters Median of 5 Memory Verdict
bcrypt cost 10 58 ms 4 KiB OWASP floor
bcrypt cost 12 225 ms 4 KiB good default
bcrypt cost 14 912 ms 4 KiB too slow to log in
scrypt N=2^16 r=8 p=1 158 ms 64 MiB needs maxmem raised
argon2id m=19 MiB t=2 p=1 25 ms 19 MiB OWASP minimum
argon2id m=64 MiB t=3 p=4 69 ms 64 MiB best value here

Argon2id dominates: at 64 MiB and t=3 it costs a quarter of bcrypt cost 12's time while forcing an attacker to allocate sixteen thousand times the memory per guess. OWASP's scrypt floor of N=2^17, r=8, p=1 needs maxmem raised from its 32 MiB default, since 128 * N * r must fit. Pick the largest parameters that keep login under about 250 ms on production hardware. bcrypt 6.0.0 and argon2 0.45.1 use libuv's thread pool (Worker Threads); their Sync variants block the event loop.