A login endpoint may verify passwords correctly and still give an attacker too many chances to guess them. If failed attempts can be repeated quickly, weak or reused passwords become easier to test through the same interface legitimate users use.

A common response is to add a rate limit. The difficult part is choosing what the limit follows. Limiting only an IP address misses distributed attempts from many sources. Limiting only an account lets one source spread attempts across many accounts. Combining the IP address and account into one key looks stricter, but creates a fresh allowance for every pair.

The useful mental model is simple: limit the resource the attacker is consuming, not merely the connection that delivered the request. For password login, that usually means protecting both the account being guessed and the service from abusive sources.

This article explains how independent account and source limits work, what threats they reduce, how to handle lockout risk, and what login throttling cannot solve.

Start with the threat you are trying to slow

Consider a password login with two inputs:

username: sam@example.test
password: [candidate password]

An online guessing attacker does not need a flaw in the password verifier. The login endpoint itself answers the useful question: did this candidate authenticate?

Two patterns matter for rate-limit design.

A targeted guessing attempt sends many candidates for one account. The sources may change:

source A -> Sam -> guess 1
source B -> Sam -> guess 2
source C -> Sam -> guess 3

A credential-stuffing or password-spraying pattern can distribute attempts across many accounts:

source A -> Sam  -> candidate
source A -> Mei  -> candidate
source A -> Luis -> candidate

The exact attacker infrastructure can vary. The defensive point is that neither the account nor the network source alone describes every useful abuse pattern.

Login throttling reduces the number or speed of online authentication attempts. It does not make a weak password stronger, protect a password database after an offline breach, stop phishing, or make a compromised session harmless. Those require other controls.

Why an IP-only limit is incomplete

Suppose the service allows ten failed login attempts per minute from each source IP address.

That helps when one machine sends a large burst. After ten failures, that source must wait.

But the account itself has no attempt budget. If requests arrive through many source addresses, each source receives its own allowance:

IP 1 -> target account -> 10 attempts
IP 2 -> target account -> 10 attempts
IP 3 -> target account -> 10 attempts
...

The source limit still raises the attacker’s cost, but it does not place a direct bound on how quickly one account can be tested across distributed sources.

There is another practical problem: an IP address is not a stable identity. Many legitimate users can share one public address through carrier networks, corporate gateways, schools, or other network address translation. Conversely, one attacker can use many addresses.

Treat source IP as useful evidence and a useful abuse-control dimension, not as proof of who is making the request.

Why an account-only limit is also incomplete

Now reverse the design. Suppose each account can receive ten failed attempts before further attempts are delayed.

That directly protects one account even if the source changes. It is therefore an important control against targeted online guessing.

But a single source can try a small number of candidates against a large number of accounts without exhausting any individual account’s budget:

source A -> account 1 -> 1 failure
source A -> account 2 -> 1 failure
source A -> account 3 -> 1 failure
...

An account limit alone does not control aggregate abusive traffic from that source.

More importantly, an account limit creates a denial-of-service trade-off. If an unauthenticated requester can deliberately exhaust another user’s account budget, a hard lock can deny that user access.

That does not mean account-based throttling is wrong. It means the response to exceeding the budget matters.

Use independent limits, not one combined key

A tempting implementation creates a single rate-limit key such as:

login:{source_ip}:{account}

This answers a narrow question: how many attempts has this source made against this account?

It does not answer either broader question:

How many attempts are targeting this account from all sources?
How many attempts is this source making across all accounts?

Every new account-source pair gets a new bucket. A distributed attack against one account can rotate sources, while a source testing many accounts can rotate account names.

Instead, evaluate independent dimensions:

account bucket: login:account:<stable-account-key>
source bucket:  login:source:<source-key>

A simplified decision looks like this:

if account_budget_exceeded:
    slow_or_reject()
else if source_budget_exceeded:
    slow_or_reject()
else:
    evaluate_login()

The exact data structure may be a token bucket, sliding window, fixed window, or another limiter. The security property comes from the scopes being independent, not from a particular rate-limit algorithm.

This example is deliberately simplified. Production systems also need concurrency-safe counters, expiry, capacity planning, failure handling, and a clear policy for distributed application instances.

Choose an account key carefully

The account dimension should follow the account being authenticated, not an attacker-controlled spelling when a stable internal identity is already known.

After the login identifier is normalized and resolved according to the application’s normal account rules, the limiter can associate failures with the stable account identifier. That avoids accidentally creating separate budgets for equivalent forms of the same login name.

Unknown account names need different treatment. Creating permanent limiter state for every arbitrary string can itself consume storage. It can also make authentication behavior reveal whether an account exists if known and unknown identifiers receive visibly different responses.

A practical design can use bounded, expiring state for unresolved identifiers while keeping externally visible authentication errors generic. The exact strategy depends on the identity model and scale of the service.

Do not let rate limiting undo account-enumeration protections. A caller should not receive a message such as “this account is now rate limited” only for real accounts if the ordinary login response intentionally hides account existence.

Prefer delay and recovery over easy permanent lockout

A hard account lock after a small number of failures gives an unauthenticated attacker a simple way to disrupt another user.

Progressive throttling is often a better starting point. For example, repeated failures can produce increasingly restrictive delays or temporary blocks while the system retains a path for legitimate recovery.

The exact thresholds should not be copied from a tutorial. They depend on password policy, multi-factor authentication, user population, traffic patterns, latency tolerance, fraud risk, and the consequences of account takeover or denial of service.

The important design relationship is:

more failures
    -> less authentication capacity for that account or source
    -> bounded recovery rather than indefinite attacker-controlled lockout

For high-value systems, stronger intervention may be justified. A service might require additional verification after suspicious failures, notify the account owner, or temporarily restrict a sensitive authentication path. Those decisions should be tied to the threat model rather than a universal failure count.

Count failures where they provide useful evidence

A login limiter needs a clear rule for what consumes the budget.

For password authentication, failed password verification is the obvious event. But avoid designs that perform expensive work without any outer traffic control. An attacker should not be able to force unlimited costly password-hash computations merely because the account-specific counter is checked only after verification.

This is one reason the source dimension is useful: it can constrain request volume before or around expensive authentication work, while the account dimension constrains repeated guessing against a particular identity.

Successful authentication usually changes the picture. A legitimate successful login is evidence that the claimant knew the required authenticator, but blindly resetting every abuse signal can also make counters easier to manipulate in complex authentication flows.

Define reset behavior explicitly. Decide which counters decay with time, which failures a successful authentication clears, and which source-level abuse signals remain independent. Test those transitions rather than relying on library defaults.

Do not make the limiter a new security bypass

Rate-limit infrastructure can fail. A remote cache can time out, a counter store can restart, or one application instance can lose access to shared state.

The correct failure behavior depends on what each limiter protects.

For a public login service, rejecting every login whenever a nonessential source-abuse counter is unavailable may create a large availability risk. Silently disabling every login defense during a limiter outage may create a security risk. The design should distinguish controls that are essential to safely evaluating an authenticator from additional traffic-shaping signals.

Possible measures include local emergency limits, bounded degraded modes, redundant limiter storage, and alerting when enforcement state is unavailable. The appropriate choice depends on service sensitivity and architecture.

The key requirement is to make the degraded behavior deliberate. Do not let an exception such as “rate-limit backend unavailable” accidentally skip authentication checks or convert a restricted path into an unrestricted one.

Rate limits must work across application instances

A per-process counter can look correct in a unit test and fail in a scaled deployment.

Suppose four application instances each allow ten account failures. If requests are distributed evenly and the counters are isolated, the effective allowance may be much larger than the policy intended.

You need enforcement state whose scope matches the policy. Common approaches include a shared limiter service or shared atomic counter store. Some architectures use coordinated edge enforcement plus application-level account controls.

Whatever implementation you choose, verify behavior through the same load-balancing path clients use. A useful test sends failures across multiple application instances and confirms that the account-level decision remains consistent.

Also test concurrent requests. A check-then-increment sequence that is not atomic can allow several simultaneous requests to observe remaining capacity before any of them records the failure.

Source identity changes at proxy boundaries

Many applications do not connect directly to the public client. A reverse proxy, load balancer, or content-delivery network may sit in front of them.

In that architecture, the application’s direct peer address may be the proxy, while a forwarded header carries the original client address. Trusting arbitrary forwarded headers from the public internet would let a requester choose the source key used by the limiter.

Establish a trusted proxy boundary first. Accept forwarded client-address information only from infrastructure that is configured to supply it, and use the framework or proxy mechanism appropriate to that deployment.

Even then, remember the earlier limitation: a client IP is a network attribute, not a user identity. It is useful for aggregate abuse control, but legitimate sharing and attacker distribution remain possible.

Observe the limiter without leaking useful details

Operators need to know whether throttling is working.

Useful internal signals include rates of failed authentication, account-limit activations, source-limit activations, unusual distributions across accounts or sources, limiter-backend errors, and legitimate recovery problems.

Keep sensitive data out of those records. Authentication logs do not need plaintext passwords, authentication secrets, or complete session tokens. Account identifiers may also require minimization or controlled access depending on the environment.

Externally, avoid exposing the internal limiter state in unnecessary detail. A generic authentication failure or generic throttling response can preserve the user experience without telling a caller which bucket fired or exactly how much budget remains.

For HTTP APIs, 429 Too Many Requests can be appropriate when the service is explicitly signaling rate limiting, but authentication applications sometimes intentionally use less distinguishable responses to reduce enumeration signals. Choose the response contract with both client behavior and information disclosure in mind.

Test the abuse patterns, not only the happy path

A limiter is easier to trust when tests model the behavior it is intended to constrain.

At minimum, verify these scenarios in a controlled environment:

one account, one source, many failures
one account, many sources, repeated failures
many accounts, one source, repeated failures
many accounts, many sources
successful authentication after throttling
counter expiry or recovery
multiple application instances
limiter storage unavailable

The expected result should follow the policy, not implementation accidents. In particular, the second scenario should exercise the account budget, and the third should exercise the source budget.

Also measure false positives. A source rule that blocks an entire office after a few mistyped passwords is operationally weak even if it slows attackers. Security controls that users regularly need bypassed tend to become less reliable over time.

Understand the residual risk

Independent account and source limits make online guessing harder by reducing how many useful authentication attempts can be made in a period. They do not guarantee that an account cannot be compromised.

An attacker may already know a valid password from another breach. Distributed infrastructure can reduce the effectiveness of source-based controls. Shared networks make aggressive source blocking costly. Weak passwords remain weak if password hashes are stolen for offline guessing.

Use login throttling as one layer in an authentication design. Strong password storage, rejection of commonly compromised passwords, multi-factor authentication appropriate to the threat model, generic authentication errors, secure session handling, and monitoring address different parts of the problem.

The practical decision is not “what is the one correct login limit?” It is which abuse dimensions must be bounded, how quickly they should recover, and how the service behaves when those limits are reached or unavailable.

Conclusion

A login rate limit is only as useful as the scope it measures. An IP-only rule can be bypassed by distributing attempts. An account-only rule can miss broad attacks and can be abused for lockout. A combined account-and-IP key gives each new pair a fresh budget.

Use independent account and source limits when both threats matter. Let the account limit follow the identity being guessed, let the source limit constrain aggregate abusive traffic, and design throttling so legitimate users can recover without giving attackers unlimited attempts.

Then verify the control under distributed sources, multiple accounts, concurrent requests, multiple application instances, and limiter failures. That turns login throttling from a simple counter into a defensible control with understood boundaries.