metric

Password Storage Work Factor

also called Hashing Cost Parameter, KDF Cost Factor

The deliberately chosen computational and memory cost of verifying one password, which sets the price an attacker pays per guess once they hold the hashes - and is the only parameter in password storage the defender actually controls.

passwordsargon2bcryptkey derivationcapacity

Password storage has exactly one assumption worth designing against: the attacker has the table. Encryption at rest does not cover that case, because a dumped table arrives through the application's own authenticated connection, already decrypted.

Given the table, the attacker's cost is guesses × cost per guess. The salt removes the economy of scale - without it, one precomputed table matches every row at once - but it does not change the cost of a single guess. The work factor is what does.

A general-purpose hash is designed to be fast; a single modern GPU computes on the order of billions of fast-hash candidates per second. A password-hashing function is designed to be slow, and the memory-hard family is designed to be expensive in memory too, so that specialised hardware gains less advantage. Argon2id, specified in RFC 9106 (2021), exposes memory, iterations and parallelism; bcrypt exposes a single cost factor. Tuned so that one verification takes about 100 ms on your hardware, the same GPU manages thousands of guesses per second rather than billions.

Why it matters

That ratio is the entire protection, and it is a number somebody chose - usually a framework default chosen years earlier on slower hardware. Six orders of magnitude separates a fast hash from a properly tuned one, which is the difference between a leaked table being fully cracked in days and only the weakest passwords falling.

It is also one of the few security parameters with a direct capacity consequence, which is why it is set too low. A memory-hard function at 64 MiB per verification with 200 concurrent logins needs about 12.8 GB of memory headroom, so the security decision lands in the capacity plan - and in a login storm, authentication is the thing that saturates.

Implementation patterns

  • Tune, do not copy. Measure on your own hardware and pick parameters giving roughly 100 ms per verification, or up to 250 ms where login rates are low. A tutorial default from 2015 is now cheap.
  • Store the parameters with the hash. Both bcrypt and Argon2 encode algorithm, cost, salt and digest in one string, so verification uses whatever that row was written with and the cost can be raised later.
  • Re-hash on next successful login when you raise the factor. There is no batch migration: the plaintext exists for a few milliseconds during authentication and nowhere else.
  • Prefer Argon2id for new systems; bcrypt remains defensible and has the longest deployment history. Avoid plain PBKDF2 for new designs unless a compliance standard requires it, since it is not memory-hard.
  • Rate-limit and lock out independently of the work factor. A high cost makes offline attack expensive; it does nothing about online guessing against one account.
  • Budget the memory, and account for the worst case - a mass password reset or a login storm after an outage.
  • Re-tune every couple of years, on a calendar reminder, because hardware moves and nobody raises this parameter spontaneously.

Industry example

The published guidance has converged: RFC 9106 (2021) gives concrete Argon2id parameter sets for different memory budgets, and national guidance such as NIST's digital identity guidelines requires salting and an intentionally expensive derivation rather than naming a single algorithm. Every large credential-breach post-mortem of the last decade reports the same split - accounts stored under a fast or unsalted scheme were cracked en masse, and accounts stored under a tuned adaptive function largely were not, with the same user population and the same passwords.

Failure scenarios

  • The 2015 default. Shipped once, never revisited, now cheap to attack. Nothing fails, nothing alerts, and the exposure is only discovered after a breach.
  • The hard-coded verification path. Parameters are compiled in rather than read from the stored string, so changing the constant makes every existing hash unverifiable.
  • Login-storm self-denial. A deliberately expensive function with no concurrency limit means a traffic spike or a credential-stuffing attack exhausts memory or CPU and takes authentication down.
  • Peppering without key management. A global secret added to the hash is a real improvement and becomes a permanent migration problem if it is ever rotated or lost.
  • Fast hash chosen because it is "strong". SHA-256 is strong and fast, and speed is the attacker's advantage here.
  • Cost raised with no re-hash path, so the parameter applies only to new users and the existing population stays at the old factor indefinitely.

Trade-offs

Choose Gains Pays
Higher work factor Linear increase in attacker cost per guess Login latency and memory per verification; capacity risk in a login storm
Memory-hard function Reduces the advantage of GPUs and custom hardware Significant memory per verification, which must be planned
Lower factor with strong rate limiting Cheap authentication path Online guessing is controlled and offline attack after a leak is not

The control's cost is paid on every legitimate login and redeemed only in a breach, which is why it loses arguments against latency unless the ratio above is on the table.

When not to use it

When you do not store password verifiers at all. Federated identity or passkeys remove this component entirely and move the work to token handling and account recovery, which is the better design for a new consumer system.

It is also the wrong lens for high-entropy secrets. An API key or a 256-bit token does not need a slow derivation, because it cannot be guessed - a single fast hash is correct there, and using an expensive function for per-request token verification is a self-inflicted performance problem.

Interview question

Q: Your consumer platform stores 40 million password hashes with bcrypt at a cost factor set in 2016. What do you change, in what order, and what capacity question do you have to answer first?

What a strong answer covers: measuring current verification time and setting a target of roughly 100 ms on today's hardware · raising the cost factor with parameters stored per row and re-hashing on next successful login · the long tail of dormant accounts and a forced reset for those below the floor after a migration window · the capacity question - memory and CPU per verification times peak concurrent logins, including a login storm - and a concurrency limit so authentication cannot be self-denied · rate limiting and lockout as a separate control for online guessing · and the honest framing that this control is paid for on every login and redeemed only in a breach.

Quick check

Quiz: Why does raising a password hashing cost factor require a re-hash at next login rather than a batch job? Because recomputing the hash needs the plaintext password, which only the user can supply and which exists only momentarily during authentication.

Flashcard: A salt and a slow key-derivation function defend against the same attacker. What does each do? - The salt removes the attacker's economy of scale across users; the work factor sets the cost of each individual guess, and only the second one makes any single password harder to crack.