Password hashing is intentionally expensive. That cost makes each offline password guess more expensive after a verifier database is stolen. The same property creates an operational risk on a live login endpoint: an unauthenticated client can ask the server to perform costly password verification again and again.

A service that treats every login attempt as unlimited work can exhaust CPU, memory, worker slots, or downstream capacity before an attacker needs a valid account. The defensive goal is not to make password hashing cheap. It is to preserve a suitable password-hashing cost while placing firm limits around how much verification work the service will accept at once.

This article presents a practical mental model for that boundary, including admission controls, unknown-account handling, capacity planning, and tests that show whether the protection still works under pressure.

Expensive verification has two security effects

A password verifier normally stores the output of a password-hashing function together with the parameters and salt needed to check a future password. Suitable password-hashing functions are designed to make guessing costly. Depending on the function, that cost can include processor time, memory, or both.

That cost is valuable when an attacker has copied password verifiers and is testing guesses independently. Every guess must pay the configured work factor.

A live authentication service has a different trust boundary:

untrusted request -> application -> password verifier -> finite compute capacity

The requester does not pay the server’s full verification cost. The server does. If request admission is effectively unlimited, a cheap stream of requests can trigger a much more expensive stream of password-hashing operations.

The threat considered here is resource exhaustion caused by repeated authentication attempts. The control is meant to keep the authentication service available and bound the compute an unauthenticated source can trigger. It does not stop offline guessing after a verifier database breach, replace strong password hashing, or prove that a request comes from a legitimate user.

Put admission control before expensive work

The useful design rule is simple: decide whether the service can accept more authentication work before starting the expensive password operation.

A simplified flow looks like this:

receive login request
        |
        v
validate basic request shape
        |
        v
apply request and account controls
        |
        v
acquire bounded verification capacity
        |
        +---- unavailable ----> reject or defer
        |
        v
run password verification
        |
        v
release capacity
        |
        v
continue authentication result handling

The capacity gate can be implemented in several ways: a bounded worker pool, a semaphore around password verification, a queue with a strict maximum size, or an equivalent mechanism provided by the application’s runtime. The exact primitive matters less than the invariant: there must be a known upper bound on concurrent expensive verification work.

Suppose one application instance can sustain 40 concurrent password checks while still leaving enough resources for normal request handling. A semaphore with 40 permits can enforce that local limit. The number 40 is only an example; production values must come from measurements on the deployed hardware and chosen password-hashing parameters.

A queue can absorb short bursts, but an unbounded queue only moves the exhaustion problem. It consumes memory, increases latency, and keeps work alive long after clients may have disconnected. If a queue is used, bound both its size and the time a request is allowed to wait.

Rate limits and concurrency limits solve different problems

A rate limit controls how many attempts are accepted over a period. A concurrency limit controls how many expensive operations can run at the same time. Authentication endpoints often benefit from both.

Consider a service that accepts at most 100 verification starts per second but allows unlimited concurrent work. If each verification takes long enough, concurrent work can still accumulate. Conversely, a strict concurrency limit can protect server capacity while one source repeatedly occupies available slots and degrades service for everyone else.

Layered controls can address different dimensions:

  • per-source controls reduce the amount of work one network source can request;
  • per-account controls reduce repeated guessing pressure against one account;
  • broader service limits protect total verification capacity;
  • bounded concurrency keeps password work from consuming every worker or memory budget.

These controls need care. Network addresses are not reliable user identities: many legitimate users can share one address, while a hostile client can operate from many addresses. Account-based limits can also be abused to interfere with a targeted user’s login if the policy simply locks an account after a small number of failures.

For that reason, treat rate limiting as resource and abuse management rather than as proof of identity. Choose responses that constrain repeated work without making account lockout an easy denial-of-service primitive.

Unknown accounts still consume security-sensitive work

A tempting optimization is to skip password hashing when the submitted account name does not exist. That saves compute, but it can create a measurable difference between existing and nonexistent accounts.

An application may instead perform a dummy password verification for unknown accounts so that both paths have broadly similar work. This can reduce account-enumeration signals based on response timing, though complete timing equality is difficult because many other parts of a request path can vary.

Dummy verification changes the resource-exhaustion calculation. An attacker does not need valid account names to trigger expensive work if unknown accounts intentionally take a hashing path.

The capacity gate therefore belongs around expensive verification regardless of whether the account exists. A useful shape is:

admit bounded work
    |
    +-- known account ----> verify against stored password verifier
    |
    +-- unknown account --> verify against fixed dummy verifier

The dummy verifier should use parameters representative of the current password policy. It must not contain a real user’s credential material. It also should not be generated afresh for every request, since generating it can add unnecessary work beyond the intended verification cost.

This pattern reduces one class of enumeration signal while preserving a bound on total verification work. It does not guarantee indistinguishable responses by itself; response bodies, status codes, database access, cache behavior, and surrounding logic can still reveal differences.

Choose password cost and service capacity together

Password-hashing parameters cannot be selected in isolation from authentication capacity. Raising a work factor increases the cost of password guesses, but it also increases legitimate login cost and the server resources consumed by hostile requests.

Measure the actual verifier on representative production hardware. Record latency and, for memory-hard functions, peak memory use under realistic concurrency. Then reserve capacity for the rest of the service rather than allowing authentication work to consume the entire machine.

A rough capacity model can make the trade-off visible. If one password check occupies 64 MiB of memory and the service permits 32 checks concurrently, those checks can account for about 2 GiB of memory before application overhead is included:

64 MiB × 32 = 2048 MiB

That arithmetic is not a configuration recommendation. Real memory behavior depends on the password-hashing function, library implementation, runtime, allocator, and surrounding application. Measure the deployed system instead of treating a paper estimate as a guarantee.

When parameters are increased, repeat load and failure testing. A setting that was operationally sound at one work factor may become an outage risk after a security-hardening change.

Fail predictably when capacity is full

Once verification capacity is bounded, the application needs an explicit overload policy. Waiting forever is not a policy.

For ordinary interactive login, a short bounded wait followed by a generic temporary-failure response can be reasonable. Another service may reject immediately when the verification pool is full. The choice depends on expected traffic, client retry behavior, and the amount of burst tolerance the service needs.

Avoid returning details that expose internal capacity, account existence, or password correctness. Also avoid retry instructions that cause every client to retry at the same instant. Where clients support it, controlled backoff with jitter can reduce synchronized retry spikes.

Overload handling should happen before expensive verification starts. A response that says “busy” only after the password hash has already consumed its resources does not protect the constrained resource.

Keep observability outside the critical failure path where practical. Metrics for rejected admission, queue depth, verification latency, and capacity saturation can help operators distinguish ordinary login failures from resource pressure. Logging must also be bounded; turning every rejected request into a large synchronous log event can create a second exhaustion path.

Common designs that move rather than remove the risk

One common mistake is to add more web workers while leaving password verification unbounded. More workers can increase the number of simultaneous expensive hashes and make memory pressure worse.

Another is to place a large queue in front of verification. This can smooth a brief traffic spike, but a large queue increases memory use and turns overload into long latency. A strict queue bound is part of the security control.

A third mistake is to apply only a per-account limit. Requests for many different account names can still consume aggregate capacity. Total service limits remain necessary when password verification is exposed to unauthenticated traffic.

It is also risky to reduce the password work factor automatically whenever the service is busy. That makes a security parameter depend on attacker-influenced load. Prefer fixed, reviewed hashing parameters and shed excess request work around them.

Finally, do not treat a capacity limit as a password-guessing defense on its own. A low-volume attacker can stay below operational thresholds. Password quality, multi-factor authentication where appropriate, credential-breach response, and abuse detection address other parts of the authentication threat model.

Test the boundary, not only successful login

A unit test that checks a correct password and an incorrect password cannot show that resource limits hold under pressure.

Test the service with enough concurrent invalid attempts to fill the verification pool. Confirm that the number of active password operations never exceeds the configured bound. Confirm that excess requests follow the intended overload path and that the queue, if present, stays within its maximum size.

Repeat the test with nonexistent account names if the application uses dummy verification. The same capacity controls should apply. Also test slow clients and disconnected requests so abandoned work does not occupy scarce verification slots longer than the implementation requires.

Watch the rest of the application during these tests. The goal is not merely to keep the login handler alive. Health checks, administrative access, monitoring, and unrelated endpoints should retain the resources required by the system’s availability target.

After changing password-hashing parameters, runtime versions, hardware classes, or authentication architecture, run the capacity tests again. Any of those changes can alter the cost represented by one verification slot.

Keep the expensive defense and bound access to it

Password hashing needs meaningful cost to resist password guessing under its intended threat model. Removing that cost to make an overloaded login endpoint faster trades one security problem for another.

A stronger design keeps the reviewed password-hashing parameters and treats verification capacity as a finite security resource. Admit work before hashing, bound concurrency and queues, combine service-wide limits with narrower abuse controls, and make overload behavior explicit.

The practical next step is to measure one password verification on production-like hardware, identify the resources it consumes, then load-test a bounded verification path. That gives the team a defensible capacity limit instead of an assumption that only becomes visible during an incident.