We follow best practices: use strong password hashing, increase cost, pick modern algorithms. That's good for security , but it comes with a hidden cost.

Every login request forces your server to do heavy work. Under normal traffic, it's fine. Under attack, it becomes a bottleneck and eventually a denial-of-service vector.

This article breaks down how that happens, and how to avoid turning your own security into a weakness.

Why We Hash Passwords

Before we talk about how this becomes a problem, let's get one thing clear why do we hash passwords at all?

Because databases get breached. And when they do, plaintext passwords are game over.

Instead of storing the actual password, we store a one-way hash. When a user logs in, we hash the input and compare it with the stored value. The original password is never saved.

Modern recommendations from OWASP go further: use slow, resource-intensive algorithms like Argon2id or bcrypt.

The goal is simple :- make brute-force attacks expensive and slow.

And this is where the tradeoff begins.

The Hidden Cost: Expensive by Design

Modern password hashing isn't slow by accident ,it's slow on purpose.

Algorithms like Argon2 and bcrypt are designed to consume CPU (and in some cases memory) to make brute-force attacks impractical. The higher the cost factor, the longer each hash takes to compute.

That's great for security. But it also means every login request carries a built-in computational cost.

One request? No problem. Hundreds? Still manageable. Thousands at once? Now you're burning CPU just to verify passwords.

And unlike most endpoints, you can't cache or shortcut this work you have to compute it every time.

This is where a secure system starts becoming an expensive one.

Turning Security Into an Attack

Let's make this concrete with a simple experiment.

Before even thinking about APIs or traffic, I measured how expensive a single password hash operation is using bcrypt:

import bcrypt
import time

password = b"correctpassword"
salt = bcrypt.gensalt(rounds=12)
start = time.time()
hashed = bcrypt.hashpw(password, salt)
end = time.time()
print(f"Hash time: {(end - start) * 1000:.1f}ms")

On my machine, this takes roughly ~180ms per hash.

That might seem small — but now scale it.

  • 1 request → ~180ms CPU time
  • 10 concurrent requests → noticeable delay
  • 100 concurrent requests → CPU starts saturating

Now imagine an attacker sending hundreds or thousands of login requests per second.

At ~180ms per request, 1000 requests/sec means your server needs 180 seconds of CPU work every second.

That's physically impossible to keep up with.

Nothing needs to be exploited. No vulnerability is required. Your system is simply overwhelmed doing exactly what it was designed to do.

Each request is cheap for the attacker…..

but expensive for the server.

That imbalance is enough.

At this point, your login endpoint effectively becomes a denial-of-service vector ,created entirely by following security best practices.

Amplifying the Attack: Payloads and Logging

CPU exhaustion alone is enough to hurt your system. But in real-world setups, things rarely stop there.

Most login endpoints don't just verify passwords,they also parse input, validate data, and often log failed attempts.

This opens up another layer of abuse.

Imagine an attacker sending login requests with unusually large payloads for example, a password field containing hundreds of kilobytes or megabytes of data.

Before your server reaches the hashing step, it has to:

  • Read the request body
  • Parse the JSON
  • Allocate memory for the input
  • Potentially log the request

If you're logging failed login attempts and especially if you log request bodies you've just added disk I/O into the equation.

Logging full request bodies in authentication endpoints is a bad practice.It exposes sensitive data like passwords and increases risk if logs are leaked.It also adds unnecessary disk I/O and can amplify performance issues under load.

Now combine everything:

  • Expensive hashing (CPU-bound)
  • Large payload parsing (memory + CPU)
  • Logging (disk I/O)

At this point, the attack is no longer just CPU-heavy it becomes a full resource exhaustion problem.

You might see:

  • CPU pinned at 100%
  • Memory usage increasing due to large inputs
  • Disk I/O spikes from logging
  • Log files growing uncontrollably

And that results to:

  • Slower responses across the system
  • Timeouts for legitimate users
  • In extreme cases, the system crashing or becoming unresponsive

All of this without exploiting a single bug.

Just by sending requests your system is designed to handle.

Security didn't fail here. The system design did.

How to Fix This (Without Weakening Security)

The problem isn't password hashing itself ,it's using it without limits.

Here's how to protect your system without compromising security:

  1. Rate limit aggressively

Limit login attempts per IP, per user, and globally. This is your first line of defense against request floods.

2. Fail fast before hashing Reject obviously invalid requests early ,empty fields, oversized payloads, malformed input. Don't waste CPU on hashing bad data.

3. Enforce request size limits Cap the maximum size of incoming requests. A password field should never be megabytes long.

4. Tune hashing cost realistically Use strong algorithms like Argon2 or bcrypt, but choose cost parameters your system can handle under load ,not just in ideal conditions.

5. Control concurrency Don't allow unlimited parallel login processing. Use worker pools or queues to cap how many hashing operations run at once.

6. Fix logging practices Avoid logging request bodies in auth endpoints. Log minimal, structured data to reduce disk I/O and risk.

7. Monitor and adapt Track CPU usage, request rates, and latency. If login traffic spikes, you should know immediately and react.

"Security isn't just about making attacks hard. It's about making your system resilient when they happen."

Conclusion

Password hashing is intentionally expensive, and that's exactly why it works. But without proper limits, that cost becomes predictable and exploitable.

The real issue isn't the algorithm. It's the lack of boundaries around it.

A login endpoint should be secure, but also controlled. Because in the end, security isn't just about resisting attacks , it's about surviving them.

A system that protects passwords but collapses under load isn't secure.

It's just fragile.