A login endpoint has to accept wrong passwords. Users mistype them, password managers can hold stale credentials, and old devices sometimes retry automatically. An attacker can use the same interface to make thousands of guesses unless the application limits how quickly authentication can be attempted.
The obvious fix is to lock an account after several failures. That slows guessing, but it creates another problem: anyone who knows a username may be able to keep that user locked out by deliberately submitting bad passwords.
Login rate limiting is therefore a balancing problem, not just a request counter. The control should make automated guessing expensive while keeping ordinary mistakes recoverable and avoiding an easy denial-of-service mechanism. This article develops that model and shows how to reason about account-based and source-based limits together.
Start with the attacker’s available attempts
For password authentication, an attacker benefits from being able to test guesses cheaply. The application does the password verification and returns a result, so an unrestricted endpoint becomes an online password-checking service.
There are several ways those attempts can be distributed. An attacker may try many passwords against one account, try one likely password against many accounts, or spread attempts across many network sources. These patterns matter because a limit attached to only one dimension can leave another path open.
A useful mental model is:
incoming login attempt
|
+--> account budget: too many attempts at this account?
|
+--> source budget: too many attempts from this source?
|
v
password verificationThe two budgets answer different questions. An account-based budget limits how quickly one account can be guessed even when requests come from changing IP addresses. A source-based budget limits how quickly one source can sweep across many accounts.
Neither signal proves that a request is malicious. A shared office, carrier network, or proxy can place many legitimate users behind one public IP address. An attacker can also distribute traffic across many addresses. Treat rate-limit keys as imperfect signals whose purpose is to constrain attack volume, not as identities.
Define the threat model before choosing thresholds
Login rate limiting mainly reduces the rate of online password guessing, password spraying, and credential-stuffing attempts that reach the authentication verifier. It buys time and raises the cost of automation.
It does not make weak or reused passwords strong. It does not stop phishing, malware, stolen sessions, offline attacks against leaked password hashes, or an attacker who already has the correct password. Multi-factor authentication can provide a separate barrier when a password is compromised, and blocking commonly compromised passwords reduces the chance that known credentials work in the first place.
Rate limiting also cannot distinguish every attacker from every legitimate user. That means the design has two failure directions:
- limits that are too loose allow too many guesses;
- limits that are too aggressive deny or frustrate legitimate authentication.
There is no portable threshold that resolves this trade-off for every application. A consumer service with millions of users, an internal administrative portal, and a low-volume business application have different traffic patterns and account-recovery costs. Choose values from the application’s threat model and observed legitimate traffic, then test them under realistic failure bursts.
Don’t rely on an IP address alone
An IP-only limit is attractive because every network request has a source address. Suppose the application permits a small number of failed attempts from an address before slowing further requests.
That can reduce a simple attack from one machine, but it has two important weaknesses.
First, attackers can use multiple network sources. If each address receives an independent budget, changing addresses creates new opportunities to guess the same account.
Second, legitimate users can share an address. A corporate proxy, mobile carrier, university network, or household may produce many logins from one public IP. A strict IP-only limit can therefore punish unrelated users for each other’s failures.
IP-based limiting is still useful. It just works better as one layer rather than the complete control.
Don’t rely on a permanent account lockout either
An account-based counter closes the distributed-source gap. If failures follow the account rather than the IP address, rotating network sources does not reset the account’s attempt history.
The dangerous version is a hard lockout that requires manual intervention after a small fixed number of failures. If an unauthenticated attacker can trigger that state repeatedly, the protection becomes a denial-of-service tool.
Prefer controls whose cost grows for suspicious authentication without unnecessarily turning a few bad requests into a long outage. Depending on the application’s risk and recovery model, that can mean temporary throttling, increasing delays, short temporary lockouts, additional verification, or a combination of these.
For example, the policy can conceptually behave like this:
few recent failures -> normal authentication
more recent failures -> slower retry rate
sustained suspicious load -> temporary restriction or extra verification
successful authentication -> update failure state according to policyThis is deliberately pseudocode rather than a recommended set of numbers. The security property comes from bounding repeated attempts while keeping recovery practical, not from copying a universal threshold.
Combine account and source limits independently
A common implementation mistake is to create one counter keyed by both account and source:
login:<account>:<ip>That looks specific, but it creates a fresh bucket for every account-and-address pair. One source can move through many accounts without exhausting a source-wide budget, and many sources can attack one account without exhausting a source-specific pair quickly enough.
Instead, evaluate independent dimensions:
account:<account-id>
source:<network-source>A request proceeds only while the relevant policies permit it. The account limit constrains concentrated guessing against one principal. The source limit constrains broad authentication traffic from one source.
In a real system, there may be additional signals such as device history or broader network reputation. Add them only when they improve a defined threat model. More signals also mean more state, more tuning, more privacy considerations, and more ways to block legitimate users accidentally.
Use a stable internal account identifier for an account bucket after the submitted login identifier has been resolved. Be careful not to turn that resolution into account enumeration: externally visible errors and timing should not unnecessarily reveal whether a username exists.
Make the response useful without exposing account state
Rate limiting changes both backend behavior and the user’s experience. A legitimate user who reaches a restriction needs a path forward, but the response should not reveal sensitive account state to an unauthenticated caller.
Avoid messages such as:
Account alice@example.test has 2 attempts remaining.That confirms an account identifier and exposes details of the protection policy. Prefer a generic authentication response that does not say whether the account exists, whether the password was wrong, or which internal rate-limit bucket was reached.
The exact HTTP behavior depends on the application’s authentication interface and clients. If the service uses 429 Too Many Requests for throttled requests, make sure clients handle it intentionally rather than retrying immediately in a tight loop. A browser flow may instead render a generic wait-and-retry message. What matters is that the external behavior does not become a detailed oracle for account existence or defensive thresholds.
Rate limiting should also happen early enough to save expensive authentication work when a request is already over an applicable limit. Password verification is intentionally computationally costly; repeatedly performing it for traffic that will be rejected wastes resources and can amplify denial-of-service pressure.
Decide what happens after a successful login
A successful authentication is strong evidence that the presented credentials were correct, but it does not automatically mean all suspicious history should disappear.
Blindly resetting every source-level counter after any successful login would let one valid account refresh a source’s ability to attack other accounts. Source controls should therefore have their own lifecycle.
For account-specific state, resetting or reducing failure history after successful authentication can improve usability, but the exact choice depends on the control. A sliding-window counter naturally expires old failures. An increasing-delay design may reduce its penalty after success. A high-risk service may retain security telemetry even when the enforcement counter resets.
Keep enforcement state separate from audit history. Clearing a throttle should not require deleting the event data needed to investigate a burst of failed logins later.
Treat storage and concurrency as part of the control
A rate limit that works on one application process but not across the deployment is easy to bypass accidentally. If requests can reach several servers, each server maintaining an independent counter may multiply the effective attempt budget.
The enforcement state therefore needs semantics that match the deployment. That can mean a shared rate-limit service, a shared datastore with atomic updates, or an edge control that consistently sees the relevant traffic. The specific technology matters less than the guarantee: concurrent requests must not each observe stale state and all conclude that an attempt is still allowed.
Also define what happens when the rate-limit dependency fails. Failing completely open preserves availability but can remove the guessing control exactly when attackers create load. Failing completely closed can block every login during an infrastructure incident. The right fallback depends on application sensitivity and architecture; make it an explicit availability-versus-abuse decision rather than an accidental exception path.
Verify the policy with attack-shaped tests
Testing only six wrong passwords from one IP are blocked gives false confidence. The useful tests follow the dimensions the policy claims to control.
With dedicated test accounts, verify at least these behaviors:
- repeated failures against one account become constrained even when the simulated source changes;
- one source cannot avoid its source-wide limit merely by changing usernames;
- unrelated sources are not unnecessarily blocked by one account’s failures;
- a successful login changes only the state that the policy says it changes;
- expired windows or temporary restrictions recover as designed;
- concurrent attempts cannot exceed the intended budget because of race conditions;
- responses do not reveal whether an account exists or which internal limit fired.
Also measure the control in production. Authentication failures, throttling decisions, and unusual volumes are useful security signals. Record enough structured context to distinguish account-targeted and source-wide patterns, while avoiding plaintext passwords, authentication tokens, or other secrets in logs.
Metrics matter because a limit can be technically active and still ineffective. If almost no malicious traffic reaches a threshold, the policy may be too loose or keyed incorrectly. If legitimate users frequently hit it, the policy may be too aggressive or the application may have a retrying client that needs correction.
Understand the limits of login rate limiting
A well-designed throttle changes the economics of repeated online attempts, but it should not carry the whole authentication design.
Strong password storage protects a different boundary: it reduces the usefulness of a stolen password database. Compromised-password screening reduces acceptance of credentials attackers are likely to already know. Multi-factor authentication can reduce the chance that possession of a password alone is enough to sign in. Security monitoring helps detect patterns that static limits miss.
There is also a usability trade-off in every additional challenge. CAPTCHAs or step-up checks can add friction and accessibility costs, so they are better treated as deliberate defense-in-depth controls than automatic substitutes for a sound rate-limit design.
The simpler design is sufficient when the threat and traffic are simple: independent account and source limits, bounded temporary restrictions, generic responses, and useful monitoring. More adaptive controls are justified when the application has enough risk and traffic diversity to benefit from them and enough operational maturity to tune them safely.
Make every retry consume a meaningful budget
The practical goal is not to make failed logins impossible. It is to ensure that repeated authentication attempts consume a bounded resource the attacker cannot cheaply reset.
Start with independent account and source limits, make restrictions temporary and recoverable where the threat model permits, and test distributed as well as single-source attempts. Then watch real authentication traffic and adjust the policy when evidence shows that legitimate users or attackers behave differently from the assumptions.
A login rate limit is working when changing an IP address does not restore unlimited guesses, changing a username does not restore unlimited source capacity, and ordinary user mistakes still have a predictable recovery path.