skip to content

Compare bcrypt, scrypt, Argon2 and PBKDF2 as password-storage functions. On what axes do they actually differ, and how would you choose and parameterise one for a service handling heavy login traffic?

level: seniorimportance: should knowfreq 52%

answer

  1. attacker advantage = parallelism; memory is what blocks it
  2. PBKDF2 cheap in memory → best case for custom hardware
  3. bcrypt: cost exponent, fixed small footprint, 72-byte input limit
  4. scrypt/Argon2: tunable memory = attacker silicon cost
  5. parameters from measured latency x peak concurrency; store per row

basics

~20 s

They differ in what resource they force the attacker to spend. PBKDF2 costs only iterations of a fast hash, so specialised hardware wins; bcrypt costs a modest fixed memory footprint; scrypt and Argon2 let you demand large tunable memory, which is what neutralises massively parallel hardware. Parameters come from a measured latency budget at peak concurrency.

solid answer

~60 s

The axis that matters is **which resource the attacker must buy**. *PBKDF2* iterates a general-purpose hash. Cheap in memory and perfectly parallel, so graphics and custom hardware get near-full advantage; it survives mainly on standardisation and certification acceptance. *bcrypt* uses a cost exponent and touches a small fixed working set (a few kilobytes). That footprint alone historically hurt parallel hardware far more than PBKDF2 did, but it is not tunable, and bcrypt only consumes the first 72 bytes of input. *scrypt* introduced tunable memory hardness: raising the memory parameter multiplies attacker silicon cost, not just time. *Argon2* makes time, memory and parallelism independent parameters; the id variant blends data-independent and data-dependent access to resist both side-channel and time-memory trade-off attacks, and is the usual default choice today. Parameterisation is a defender-budget exercise: measure on production-class hardware, target a fraction of a second per verification at peak concurrency, keep total memory below what concurrent logins can exhaust, store the parameters per record, and re-derive on successful login when a row is below policy.

go deeper

for a junior

Know the names and the one-line ordering: prefer a modern memory-hard function; any of them beats a plain fast hash; parameters exist and matter.

for a middle

Explain what each function makes expensive and why memory hardness blocks parallel hardware, plus bcrypt's input-length limit.

for a senior

Drive the parameterisation: measure on production-class hardware, budget memory times peak concurrency, protect the endpoint from cost-amplification abuse, and store parameters per record for opportunistic upgrade.

for a principal

Frame it as a long-lived cost policy with a migration mechanism and a compliance dimension, and be explicit that the residual risk lives with dormant accounts and weak human passwords rather than with the function choice.

## The real axis: which resource the attacker must buy Every password function makes one derivation expensive. They differ in *what* is expensive, and that determines how much the attacker's specialised hardware helps them. An attacker's advantage comes from parallelism. A graphics card runs thousands of small cores; custom hardware can instantiate an arithmetic pipeline thousands of times. What limits that replication is not arithmetic — silicon is cheap at arithmetic — but **memory**. Fast local memory per core is expensive and scarce. So a function that demands a large working set per evaluation cannot be replicated thousands of times on one chip, and the attacker's advantage collapses toward the defender's. ## The four functions on that axis **PBKDF2** is iteration of a general-purpose pseudorandom function (typically an HMAC over a SHA family hash) a configured number of times. Memory use is trivial and the work is exactly the operation hardware vendors have optimised hardest, so an attacker's per-guess advantage over a defender's general-purpose CPU is at its maximum here. Its virtues are non-technical and real: it is old, standardised, implemented everywhere, and accepted by certification regimes that have not yet blessed newer functions. If it must be used, the iteration count has to be pushed far higher than for the alternatives to reach comparable attacker cost, and it will still fare worse against custom hardware. **bcrypt** derives from a block cipher key schedule that repeatedly accesses a working state of a few kilobytes. Cost is set by an exponent, so each increment doubles the work. The fixed memory footprint is small in absolute terms but was historically enough to hurt parallel implementations meaningfully, because thousands of cores each needing a few kilobytes of fast memory exhausts on-chip memory quickly. Its limitations are that the footprint is *fixed* — it cannot be raised as attacker hardware grows — and that the algorithm consumes only the first 72 bytes of its input, so longer passphrases are silently truncated. Some implementations also mishandle non-ASCII input or embedded null bytes, which is why pre-hashing the password to a fixed-length value before bcrypt is a common pattern (and must be done carefully, base64-encoding the intermediate value so no null byte truncates it). **scrypt** was designed explicitly to add tunable memory hardness: it fills a large array using a sequential process, then reads it in a data-dependent order, so a low-memory implementation must recompute values and pays a time penalty. Raising the memory parameter raises the attacker's *hardware* cost, not merely their time — that is the qualitative advance. **Argon2** generalises this with three independent parameters: iterations (time), memory, and parallelism (lanes). Its variants matter. The data-independent variant addresses side-channel exposure, because data-dependent memory access patterns can leak information through cache timing to an attacker sharing hardware. The data-dependent variant maximises resistance to time-memory trade-offs. The hybrid — commonly the recommended default — runs the first pass data-independently and later passes data-dependently to get most of both properties. ## Choosing In the absence of a constraint, choose the memory-hard hybrid (Argon2id) with well-reviewed parameters. Choose bcrypt when the ecosystem's library support and operational familiarity dominate, accept the input-length limitation, and know that the memory floor is fixed. Choose PBKDF2 when a compliance regime requires it, and compensate with a much higher iteration count. scrypt is a reasonable choice where it is already established. The differences between the three modern options are far smaller than the difference between any of them and a bare fast hash, so a team stuck arguing about which one is usually optimising the wrong decision. ## Parameterising for heavy login traffic Parameters are chosen from the **defender's** budget, because the defender pays them on every authentication. Start by measuring on hardware representative of production, not a laptop. Pick a target verification latency — a fraction of a second is the usual band — and then apply the concurrency constraint that most teams forget: the cost is paid *per concurrent login*, and memory-hard functions multiply memory as well as CPU. A memory parameter that is comfortable at one login per second can exhaust a container's memory at a hundred, and the resulting failure is a login outage. So compute peak concurrent verifications, multiply by the memory parameter, and check that against the process's real budget with headroom. Degrade the parallelism or memory setting rather than shipping a configuration that only works at idle. The second coupling is **denial of service**. An expensive verification is a free amplifier for anyone submitting wrong passwords, so the login path needs rate limiting per account and per source, and ideally a cheap pre-check that rejects obviously invalid requests before the derivation runs. Never skip the derivation for unknown usernames, though — that produces a timing oracle for account enumeration; run a dummy derivation instead. Finally, build the **upgrade path** in from the start. Store the algorithm identifier, parameters and salt with every record. On a successful login, if the stored parameters are below current policy, re-derive from the plaintext you momentarily hold and replace the record. Review the policy periodically as hardware moves. A system that stores a bare digest column has no way to do any of this and will eventually face a forced global reset.

  • Why is memory hardness a stronger property than simply increasing iteration count?
    Iterations raise the attacker's time linearly while leaving each evaluation tiny, so the attacker recovers the loss by replicating thousands of evaluation units on one chip. Memory is the resource that limits replication: fast per-core memory is scarce and expensive, so demanding a large working set per evaluation directly caps how many evaluations fit on a device. It converts the defence from a time cost into a hardware cost.
  • What goes wrong with bcrypt and very long passphrases, and how do you handle it?
    bcrypt consumes only the first 72 bytes of input, so everything beyond that is ignored and two long passphrases sharing a prefix verify identically. The usual mitigation is to pre-hash the password with a general-purpose hash and encode the result — base64, so that no null byte truncates the string in implementations that treat input as C strings — then feed that fixed-length value to bcrypt. Choosing a function without the limit avoids the issue entirely.
  • You raise the work factor. What happens to the passwords already stored under the old parameters?
    They keep verifying, because each record stores the parameters it was created with. The upgrade happens opportunistically: on a successful login you hold the plaintext for an instant, so you re-derive under the new policy and replace the record. Accounts that never log in stay on old parameters, which is an accepted residual risk usually handled by a deadline plus a forced reset for the stragglers.

saying these in an interview costs you the question

  • Treating the choice among modern functions as more important than moving off a fast hash
  • Tuning parameters on a developer laptop and shipping them to production
  • Ignoring the memory-times-concurrency budget, so peak login traffic exhausts the process
  • Skipping the derivation for unknown usernames, creating a timing oracle for enumeration
  • Storing a bare digest with no algorithm or parameter metadata, leaving no upgrade path

context