Rate limiting on a login page means the server counts failed sign-in attempts and, once they cross a threshold, refuses to keep answering — typically by returning HTTP 429 Too Many Requests with a Retry-After header. That single response is what stops the two most common automated password attacks: brute force (guessing one account's password) and credential stuffing (replaying passwords leaked from other breaches). Without a limit, a bot can try thousands of guesses per second with no pushback, and the fix is usually a few lines of middleware.
This guide explains what to protect, how much is enough, and exactly how to add it in the common backend frameworks. It is grounded in the OWASP Authentication Cheat Sheet, the OWASP Credential Stuffing Prevention Cheat Sheet, and RFC 6585, which defines the 429 status.
For the site owner (plain English)
If you run a site with a login page — a members area, a customer portal, a WordPress admin — think of rate limiting as a bouncer who counts how many times someone gets the password wrong. A real person mistypes their password once or twice. A bot tries millions of combinations. Rate limiting lets the first through and shuts the door on the second: after a handful of failures from the same source, the server stops answering for a short cool-down. You do not need to know the code to know whether it exists — you can test your own login page in seconds, and if it is missing you can hand this guide to whoever maintains the site. The cost to add it is minutes of work; the cost of skipping it is that any attacker with a laptop can run an automated account-takeover pipeline against your users.
What login rate limiting is
An authentication endpoint is any URL that accepts a username and password and decides whether to grant a session — /login, /api/auth/login, /oauth/token and their equivalents. Rate limiting is a counter attached to that endpoint: it tracks how many requests (or how many failed requests) arrive from a given IP, a given account, or both, inside a time window. When the count crosses the limit, the server returns 429 instead of processing the attempt, and it tells the caller when to try again via Retry-After.
The point is not to punish real users — someone who forgets a password should still be able to try a few times. The point is to make automation uneconomical. A human tries a few passwords a minute; a script tries thousands a second. Any limit low enough to catch the script but high enough to permit an honest retry closes the gap.
The two attacks it stops
Brute force targets one account and tries password after password until one works. It is defeated by a per-account limit: after 5–10 failures against alice@example.com, further attempts against that account are refused for a cool-down, so the attacker can never grind through a large password list in a useful amount of time.
Credential stuffing is the more common attack today. Instead of guessing, it replays real email/password pairs stolen from other sites' breaches, betting that people reuse passwords. Each pair is tried only once or twice, so a per-account limit barely notices — but the attacker fires the same list at thousands of accounts, so a per-IP limit and infrastructure-level throttling catch the volume. This is why good defence uses both dimensions. The full mechanics are in the credential-stuffing prevention guide.
At a glance: the three companion guides
This pillar is the umbrella. Three focused guides go deeper on each part of the problem — the attacks, and the signal the checker looks for.
| Guide | What it covers |
|---|---|
| How to stop brute-force attacks | Owner-facing, step-by-step hardening of a login page against password-guessing bots. |
| Credential-stuffing prevention | What credential stuffing is, why reused passwords enable it, and the layered defence. |
| HTTP 429 Too Many Requests | What the 429 status and Retry-After header mean, and how to return them correctly. |
How much rate limiting is enough?
There is no single magic number, but the OWASP guidance and common practice converge on a few sensible defaults. Treat these as starting points and tune to your traffic:
- Per account: begin slowing or challenging after 5–10 consecutive failures for the same username, then apply a cool-down that grows on repeat abuse.
- Per IP: a broader ceiling — for example 20–100 login attempts per IP per few minutes — catches a single source hammering many accounts.
- Exponential back-off: double the cool-down on each subsequent breach, so an attacker who rotates IPs still faces ever-increasing delay.
- Prefer throttling to hard lockout where you can. A permanent account lockout after N failures lets an attacker lock out a legitimate user on purpose (a denial-of-service). A temporary slow-down or a CAPTCHA avoids that.
Layered defence, not one control
Rate limiting is the foundation, but the strongest login pages stack independent layers so a bypass at one does not compromise the rest:
1. Return 429 after a threshold
The core control. After 5–10 failures per IP or per account, respond 429 with Retry-After instead of processing the attempt. Every mainstream framework has middleware for this — see the developer section below.
2. Challenge with a CAPTCHA
After the second or third failure, present a CAPTCHA (Cloudflare Turnstile, hCaptcha). This breaks automation without locking out the real user, who simply solves it and continues.
3. Multi-factor authentication
MFA is the backstop: even a correct stolen password does not grant a session without the second factor. It is the single most effective defence against credential stuffing.
4. Infrastructure-level throttling
A WAF or edge platform (Cloudflare, AWS WAF, Azure Front Door) can absorb high-volume bursts before they ever reach your application, and adds bot-detection you would not build yourself.
Developer: exact syntax per framework
Below is the minimal, framework-native way to rate-limit a login route in the common stacks. The goal in every case is the same: count attempts and return 429 with Retry-After once the limit is hit.
Node / Express — express-rate-limit
import rateLimit from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 10, // 10 attempts per IP per window
standardHeaders: true, // sends RateLimit-* and Retry-After
legacyHeaders: false,
message: { error: "Too many login attempts, try again later." },
});
app.post("/login", loginLimiter, loginHandler); // returns 429 when exceededDjango — django-axes
# settings.py
INSTALLED_APPS += ["axes"]
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesStandaloneBackend",
"django.contrib.auth.backends.ModelBackend",
]
AXES_FAILURE_LIMIT = 5 # lock after 5 failures
AXES_COOLOFF_TIME = 1 # hours before reset
AXES_LOCKOUT_PARAMETERS = ["ip_address", "username"] # per IP + per accountJava / Spring Security
// Track failures on AuthenticationFailureBadCredentialsEvent, then block:
@Component
public class LoginAttemptService {
private final LoadingCache<String, Integer> attempts =
CacheBuilder.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.build(CacheLoader.from(k -> 0));
public void loginFailed(String key) { attempts.put(key, attempts.getUnchecked(key) + 1); }
public boolean isBlocked(String key) { return attempts.getUnchecked(key) >= 10; }
}
// In a filter, if isBlocked(ip) -> response.setStatus(429) + Retry-After header.Ruby / Rack::Attack
# config/initializers/rack_attack.rb
Rack::Attack.throttle("logins/ip", limit: 10, period: 15.minutes) do |req|
req.ip if req.path == "/login" && req.post?
end
Rack::Attack.throttle("logins/account", limit: 5, period: 15.minutes) do |req|
req.params["email"].to_s.downcase.presence if req.path == "/login" && req.post?
end
# Rack::Attack returns 429 automatically when a throttle trips.Raw HTTP — the response any of these should emit
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{"error": "too_many_requests", "retry_after": 60}How to check your own login page
You can test this passively without touching your code. The free SecScan Login Rate Limit Checker sends a small burst of requests with provably fake credentials and reports whether the server ever returns a 429 or Retry-After — the signal that a limit exists. It cannot log in to anything and only tests domains you point it at.
See whether your login page rate-limits failed attempts — free, no signup.
Run the login rate limit checker →From here, go deeper: read how to stop brute-force attacks, the concept and defence of credential stuffing, and what the HTTP 429 response actually means when the checker reports it.