Tune Argon2id Password Hashing Parameters
Pick memory, iterations, and parallelism from your hardware budget and target latency, then verify the choices against both login UX and attack cost.
Task
Choose and verify Argon2id parameters that keep login under your latency budget while maximizing attacker cost per guess.
When to use this
Password hashes were configured with defaults from a tutorial in 2019. Login is fast, and so would be an offline cracking rig.
Prerequisites
- argon2 available in your language runtime, such as @nodejs/argon2 or argon2-cffi
- Knowledge of the login server memory and CPU budget
- A realistic target login latency agreed with product
Steps
1. Benchmark candidate parameters on login-sized hardware.
const argon2 = require("@nodejs/argon2");
const t0 = performance.now();
await argon2.hash("correct horse", {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
console.log("ms:", performance.now() - t0);
Expected output: The timing for the candidate configuration, for example 45 ms, is the cost per login on real hardware.
2. Scale memory to the budget and re-measure.
await argon2.hash(pw, { memoryCost: 65536, timeCost: 3, parallelism: 4 });Expected output: Raising memory to 64 MiB with three iterations lands around 200 ms, the OWASP-recommended neighborhood for interactive login.
3. Encode the parameters into every hash string.
const stored = await argon2.hash(pw, opts);
// $argon2id$v=19$m=65536,t=3,p=4$<salt>$<digest>
Expected output: Parameters travel with the hash, so future tuning can differ per user and verification reads them per hash.
4. Verify a hash with the same library and check the timing budget.
node -e 'require("@nodejs/argon2").verify(stored, pw).then(() => console.log("ok"))' && ab -n 100 -c 10 -T application/json https://api.example.com/loginExpected output: Verification succeeds and the login endpoint sustains its concurrency target within the latency budget.
5. Migrate existing users on next login.
const needsRehash = argon2.needsRehash(stored, opts);
if (needsRehash) {
const fresh = await argon2.hash(password, opts);
await users.updateHash(userId, fresh);
}
Expected output: Old-cost hashes are upgraded transparently as users log in, without a mass-reset campaign.
Troubleshooting
Problem
Login latency budget exceeded
Solution
Lower timeCost first, then memory; parallelism absorbs memory cost on multicore boxes, and 100 ms of security is worth more than 20 ms of snappiness.
Problem
Memory cost crashes constrained containers
Solution
The hash allocates the full memory per verification; size the container limit above memoryCost times peak concurrent logins, or queue logins.
Problem
Different runtimes produce incompatible hashes
Solution
Stick to argon2id with the standard encoded format; the encoded parameters make hashes portable across compliant libraries.
Limitations
- Interactive login bounds parameters; server-to-server verification can afford much stronger ones.
- Hash strength cannot rescue weak passwords; pair Argon2id with breach-password checks and rate limiting.
Buy attacker time with your users time
Password hashing is an exchange rate: every millisecond your users spend logging in is a multiple an attacker must spend per guess. Argon2id parameters are the dial, and memory is the strongest part of it, because GPUs trade compute cheaply but RAM expensively. Tune on the actual login hardware, hold the line around 200 milliseconds, and encode the parameters in every stored hash so they can strengthen over time.
const stored = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MiB — the expensive part for GPUs
timeCost: 3,
parallelism: 4,
});
// stored: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<digest>
Memory is the asymmetric weapon
An attacker with a rack of GPUs can brute-force iterations cheaply, but each GPU core has limited fast memory. Argon2id requiring 64 MiB per guess forces the rig to parallelize with memory, not cores, cutting guesses per second by orders of magnitude compared to a low-memory hash at the same latency. When budget forces a choice, spend it on memoryCost first.
Upgrade hashes lazily, never en masse
The encoded hash carries its parameters, so verification works for every historical configuration, and needsRehash tells you when a stored hash is below current policy. Upgrade on successful login, transparently, and the whole user base migrates to stronger parameters over one natural login cycle with no reset emails and no support tickets.