Cutting the Cost of Password Hashing at Scale

Updated 2026-03-27 · By SPUNK13 · Back to spunk.bet

Password hashing is the one part of your stack where slowness is the feature. That makes it the only CPU cost you cannot optimise away — you can only spend it deliberately. The goal is to pick parameters that cost an attacker a fortune and cost you a predictable, budgeted amount of CPU per login.

Know Which Hashes Are Supposed to Be Fast

SHA-256, BLAKE3 and xxHash are fast by design and belong in checksums, content addressing, HMACs and cache keys. A modern CPU does SHA-256 at gigabytes per second with hardware instructions, and a GPU farm does far more — which is exactly why using SHA-256 for passwords is a vulnerability rather than an optimisation. Password hashing needs a deliberately slow, memory-hard function: Argon2id, scrypt, or bcrypt.

Tune bcrypt by Measuring, Not by Copying

bcrypt's cost factor is an exponent: cost 12 does twice the work of cost 11. The right way to choose it is to measure on the hardware you will actually run on and target a budget — commonly 100–250 ms per hash for an interactive login.

python - <<'EOF'
import bcrypt, time
pw = b"correct horse battery staple"
for cost in (10, 11, 12, 13, 14):
    t = time.perf_counter()
    bcrypt.hashpw(pw, bcrypt.gensalt(cost))
    print(cost, round((time.perf_counter()-t)*1000), "ms")
EOF

Two bcrypt gotchas that are not about performance: it silently truncates input at 72 bytes, and some implementations truncate at the first NUL byte. If you pre-hash long passwords, hash to base64 rather than raw bytes to avoid the NUL issue.

Argon2id Parameters in Plain Terms

Argon2id takes memory, iterations and parallelism. Memory is the parameter that actually hurts attackers, because GPUs have far less memory per core than they have cores. OWASP's current guidance is a reasonable starting point: 19 MiB with 2 iterations and parallelism 1, or 46 MiB with 1 iteration. Higher memory is better if you can afford it.

The capacity arithmetic is straightforward and worth doing before launch. At 64 MiB per hash and 200 ms per hash, one core sustains five logins per second and each concurrent hash holds 64 MiB. Ten concurrent logins is 640 MiB of RAM doing nothing but hashing. Size your login pool accordingly, or you will discover this during a traffic spike.

The Real Cost Savings

Upgrade Costs Without a Migration

Modern hashes store their parameters in the encoded string, so on successful login you can check whether the stored cost is below your current target and transparently re-hash with the new parameters while you still have the plaintext in memory. Every library exposes this — needs_rehash, password_needs_rehash, or a version check on the prefix. It costs one extra hash on a fraction of logins and removes the need for a big-bang migration.

What Not to Build

Do not write your own key derivation, do not use a global pepper you cannot rotate, do not store the salt separately (every modern format embeds it), and do not truncate the output to save bytes. A bcrypt hash is 60 characters and an Argon2id string is under 100; storage is not the cost worth cutting here.

Want More?

Explore our complete collection of free tools and resources.

Visit spunk.bet
400+ Free Tools Free Casino Meme Generator Birth Charts Scam Database BPM Calculator