Try the Bcrypt Generator

Your Password Hash Is a Denial-of-Service Amplifier

A deliberately slow hash makes your login endpoint a denial-of-service amplifier. Rate limiting before hashing, dummy hashes for unknown users, and async execution fix it.

September 21, 2026 7 min read
Share: Facebook WhatsApp LinkedIn Email
Your Password Hash Is a Denial-of-Service Amplifier

The Login Endpoint That Takes Down the Server

bcrypt is designed to be slow. At a sensible cost factor, a single hash takes somewhere around a quarter of a second of CPU time. That's the point — it makes offline guessing against stolen hashes expensive.

It also means your login endpoint has a built-in amplification factor.

An attacker sending 100 login requests per second to a server with four CPU cores, each request costing 250 ms of CPU, demands 25 seconds of CPU work every second. The server can supply 4. The login endpoint saturates, and everything sharing that server slows with it.

No vulnerability required. Just a correctly implemented, deliberately slow hash function and an unthrottled endpoint.

The Asymmetry

The attacker's cost per request is negligible — a small HTTP request. Your cost is a full bcrypt computation.

That asymmetry is exactly what you want against offline attacks, where the attacker is the one computing hashes. It's exactly what you don't want against online attacks, where you're computing them on the attacker's behalf.

The cost factor you choose for security against offline cracking directly sets the price of a denial-of-service against your login.

Each increment of the cost factor doubles the work:

cost 10:  ~60–80 ms   (hardware-dependent)
cost 11:  ~120–160 ms
cost 12:  ~250–300 ms
cost 13:  ~500–600 ms
cost 14:  ~1 second

Timings vary substantially with hardware — measure on your own servers rather than trusting any published table.

The Bcrypt Generator lets you see the cost effect directly:

  1. Enter a test string.
  2. Generate at cost 10, then 12, then 14.
  3. Note how generation time changes.

The doubling is very noticeable above 12.

Choosing a Cost Factor

The conventional guidance is to choose the highest cost your servers can sustain while keeping login latency acceptable — often expressed as a target of roughly 250 ms per hash.

The DoS consideration adds a second constraint: your peak legitimate login rate.

hashes per second per core ≈ 1 / hash time
server capacity ≈ cores × (1 / hash time)

At 250 ms per hash on 8 cores, you can process about 32 logins per second before saturation. If legitimate peak traffic approaches that, you have no headroom for an attack — or for a morning login spike.

Practical approach:

  1. Measure hash time at candidate cost factors on production hardware
  2. Estimate peak legitimate login rate
  3. Choose a cost that leaves substantial headroom at peak
  4. Add rate limiting regardless — which is what actually addresses the attack

Rate Limiting Is the Real Defence

Cost factor tuning manages capacity. Rate limiting manages abuse.

Per-account limits. Cap attempts against any single username — for example, a small number of failures before a delay or lockout. This defends against targeted guessing.

Per-source limits. Cap attempts from any single IP address. This defends against a single source hammering the endpoint.

Global limits. Cap total login hash operations per second across the system. This protects capacity regardless of how the attack is distributed.

All three are needed, because each has a gap the others cover:

  • Per-account limits alone let an attacker spray one password across thousands of accounts
  • Per-source limits alone are defeated by distributed attacks from many addresses
  • Global limits alone lock out legitimate users during an attack

Reject before hashing. The crucial implementation detail: check rate limits before computing the hash. A rate limit that rejects the request after doing the bcrypt work has already paid the cost.

Use exponential backoff for repeated failures rather than hard lockout. A permanent lockout lets an attacker deliberately lock legitimate users out of their accounts by failing on purpose. Progressive delays slow guessing without handing attackers a way to deny service to specific people.

Don't Leak Which Accounts Exist

A related timing issue.

A naive login handler:

user = find_user(username)
if user is None:
    return "invalid credentials"        # fast — no hash computed
if not bcrypt.verify(password, user.hash):
    return "invalid credentials"        # slow — hash computed

Both paths return the same message. They take very different amounts of time. A nonexistent user returns in milliseconds; an existing one takes the full bcrypt duration.

An attacker measuring response time can enumerate valid usernames without ever guessing a password.

The fix is to always compute a hash:

user = find_user(username)
hash_to_check = user.hash if user else DUMMY_HASH
valid = bcrypt.verify(password, hash_to_check)
if user is None or not valid:
    return "invalid credentials"

DUMMY_HASH is a precomputed bcrypt hash at the same cost factor. Every request pays the same cost regardless of whether the user exists.

This does increase the work per invalid request — which is exactly why rate limiting has to sit in front of it.

The same principle applies to password reset and registration flows. "No account with that email" versus "reset link sent" leaks existence directly, before timing even enters the picture. Returning the same message for both closes it.

Keep Hashing Off the Request Thread

In event-loop runtimes — Node.js being the common case — a synchronous bcrypt call blocks the entire process for its duration. Every other request waits.

// Blocks the event loop for ~250 ms
bcrypt.compareSync(password, hash);

// Runs on a thread pool
await bcrypt.compare(password, hash);

Always use the asynchronous variant. The synchronous one turns every login into a full-process stall.

Even asynchronous hashing uses a thread pool of limited size. Under heavy login load, the pool saturates and other operations sharing it — file I/O, DNS lookups, certain crypto operations — queue behind the hashes. Isolating authentication onto separate infrastructure is worth considering at scale, so a login surge can't degrade unrelated functionality.

Upgrading Cost Factors Over Time

Hardware gets faster, so a cost factor that was appropriate years ago provides less protection now. Increasing it doesn't require a password reset.

bcrypt hashes store their cost factor in the hash string. On successful login:

if bcrypt.verify(password, stored_hash):
    if cost_of(stored_hash) < CURRENT_COST:
        new_hash = bcrypt.hash(password, CURRENT_COST)
        save(new_hash)

Users migrate to the new cost as they log in. Accounts that never log in stay on the old cost, which is a reasonable tradeoff.

Account for the capacity change before increasing. One increment doubles the CPU cost of every login — make sure the rate limits and capacity estimates are updated to match.

Practical Tips

Measure hash time on production hardware at each candidate cost factor.

Rate limit per account, per source, and globally.

Check rate limits before computing the hash.

Always compute a hash, using a dummy for nonexistent users, to avoid timing-based enumeration.

Return identical messages for "no such user" and "wrong password."

Use asynchronous hashing in event-loop runtimes.

Prefer progressive delays over hard lockout.

Upgrade cost factors on login as hardware improves, after checking capacity.

Monitor login endpoint CPU — a spike is often the first sign of an attack.

FAQ

Can bcrypt cause a denial of service? Its deliberate slowness means each login request costs significant CPU. Without rate limiting, a modest request rate can saturate a server.

What cost factor should I use? The highest your hardware sustains at peak legitimate load with substantial headroom — often around 250 ms per hash. Measure on your own servers.

Why hash a password for a user that doesn't exist? To make response time identical whether or not the account exists, preventing username enumeration by timing.

Should I lock accounts after failed attempts? Progressive delays are generally better. Hard lockout lets attackers deliberately lock out legitimate users.

Why use async bcrypt in Node.js? The synchronous version blocks the entire event loop, stalling every concurrent request for the duration of the hash.

How do I increase the cost factor for existing users? Rehash on successful login when the stored hash's cost is below the current target. The cost is stored in the hash itself.

The Takeaway

bcrypt's slowness is a security property against stolen hashes and an operational liability against live traffic. The cost factor sets both, which is why it can't be chosen in isolation — rate limiting in front of the hash, constant-time handling of unknown users, and asynchronous execution are what let you run a strong cost factor without handing attackers an easy way to take down your login.

Generate and test bcrypt hashes at different cost factors free with the Bcrypt Generator at sadiqbd.com — no sign-up, instant results.

Ask AI about this article
Share: Facebook WhatsApp LinkedIn Email

Bcrypt Generator

Free, instant results — no sign-up required.

Open Bcrypt Generator →
Similar Tools
URL Encoder/Decoder Cron Explainer HTML Entities Number Base Converter JWT Decoder Base64 Encoder/Decoder Hash Generator Regex Tester
Bcrypt's 72-Byte Limit: Why Two Different Passwords Can Hash to the Same Value
Developer
Bcrypt's 72-Byte Limit: Why Two Different Passwords Can Hash to the Same Value
bcrypt Work Factors Double With Each Increment — Here's How to Choose the Right One and Upgrade Existing Hashes
Developer
bcrypt Work Factors Double With Each Increment — Here's How to Choose the Right One and Upgrade Existing Hashes
Argon2id Won the Password Hashing Competition — Here's Why Memory-Hardness Matters and When to Migrate From bcrypt
Developer
Argon2id Won the Password Hashing Competition — Here's Why Memory-Hardness Matters and When to Migrate From bcrypt
Reading a bcrypt Hash: What $2b$12$ Means and Where the Salt Is
Developer
Reading a bcrypt Hash: What $2b$12$ Means and Where the Salt Is