Guide

How to Rate-Limit Your Login Page: Brute-Force & Credential-Stuffing Protection

Login rate limiting counts failed sign-in attempts and returns HTTP 429 once they cross a threshold, stopping automated brute-force and credential-stuffing attacks. Without it, bots can guess thousands of passwords a second; the fix is usually a few lines of framework middleware.

By Paul Rudenko, Security ResearcherUpdated Jul 1, 202610 min read

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.

GuideWhat it covers
How to stop brute-force attacksOwner-facing, step-by-step hardening of a login page against password-guessing bots.
Credential-stuffing preventionWhat credential stuffing is, why reused passwords enable it, and the layered defence.
HTTP 429 Too Many RequestsWhat 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 exceeded

Django — 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 account

Java / 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}
Rate-limit on failed attempts, or on all attempts to the login route — but do not let a successful login reset an attacker's counter for the whole IP. And make sure the limit is enforced on the API endpoint that actually checks the password, not only on the HTML form page, or a bot posting straight to the JSON API sails past it.

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.

Frequently asked questions

How do I stop brute force login attacks?

Rate-limit the login endpoint so that after a small number of failed attempts — typically 5 to 10 per account or per IP address inside a time window — the server returns HTTP 429 Too Many Requests instead of checking the password again, and tells the caller when to retry via a Retry-After header. Every mainstream framework has middleware for this: express-rate-limit for Node, django-axes for Django, Rack::Attack for Ruby, and an attempt-tracking filter in Spring Security. Layer a CAPTCHA challenge after the second or third failure and multi-factor authentication on top, so even a correct guessed password does not grant a session. Prefer a temporary throttle or cool-down over a permanent account lockout, because a hard lockout lets an attacker deliberately lock out real users. Test your own login page with a passive checker to confirm the limit actually fires on the endpoint that checks the password.

How many failed login attempts should trigger a lockout?

A common, defensible starting point is to begin slowing or challenging a login after 5 to 10 consecutive failures for the same account, and to apply a broader per-IP ceiling — for example 20 to 100 attempts per IP over a few minutes — to catch a single source attacking many accounts. There is no universally correct number; the right threshold balances catching automated attacks against not frustrating real users who mistype a password. Two refinements matter more than the exact count. First, use exponential back-off: double the cool-down each time the limit is breached again, so a persistent attacker faces ever-growing delay. Second, prefer a temporary throttle or a CAPTCHA to a permanent lockout, because an attacker who knows a username can otherwise trip a hard lockout on purpose and deny service to the real owner. Tune the numbers against your own traffic after you deploy.

What is the difference between brute force and credential stuffing?

Both are automated attacks against a login page, but they work differently and need slightly different defences. Brute force targets one account and tries many passwords against it — guessing, dictionary words, or every combination — until one works; it is defeated by a per-account limit that refuses further attempts after a handful of failures. Credential stuffing does not guess: it replays real email and password pairs stolen from other websites' breaches, betting that people reuse the same password across sites. Each pair is tried only once or twice, so a per-account limit barely notices it, but the attacker fires the same list at thousands of accounts from many IPs, so per-IP limits, infrastructure throttling, and bot detection are what catch it. Multi-factor authentication is the strongest single defence against credential stuffing, because a correct stolen password still cannot complete a login without the second factor.

Does rate limiting hurt real users?

Not if it is tuned sensibly. A real person forgets or mistypes a password once or twice, then either gets it right or uses a reset link — that behaviour sits far below any reasonable limit. An automated attack tries thousands of attempts a second, which is what the limit is designed to catch. The one design mistake that does hurt real users is a permanent account lockout after N failures: because anyone who knows a username can trigger it on purpose, it hands attackers a denial-of-service tool aimed at your legitimate users. Avoid that by using a temporary cool-down, exponential back-off, or a CAPTCHA challenge instead of a hard lock, and by rate-limiting primarily on failed attempts rather than all traffic. Combined with a Retry-After header that tells honest clients exactly when to try again, a well-tuned limit is invisible to normal users and expensive for attackers.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →