Article

How to Stop Brute-Force Login Attacks

A brute-force attack automatically tries password after password against a login until one works. The most effective defence is rate limiting: return HTTP 429 after a handful of failures so guessing is too slow to succeed, backed by CAPTCHA and multi-factor authentication.

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

A brute-force attack points an automated tool at your login page and tries password after password against an account until one works. The single most effective defence is rate limiting: after a handful of failed attempts, the server returns HTTP 429 Too Many Requests and refuses to keep checking, which makes the attack far too slow to succeed. This guide walks an owner through hardening a login page, then gives developers the exact framework syntax.

For the site owner (plain English)

Imagine a burglar trying every key on a giant keyring against your front door, one after another, all night. That is a brute-force attack: a program tries millions of passwords against a login until it finds the right one. The defence is not a stronger door — it is a lock that jams after a few wrong keys and refuses to open for a while. On a website that means counting failed logins and, after five to ten, telling the source to slow down and go away. A real customer who mistypes their password twice is unaffected; a bot trying thousands a second is stopped cold. You do not need to write the code to know whether the lock exists: test your login page, and if nothing pushes back after repeated failures, forward the developer section below to whoever maintains the site.

How a brute-force attack works

The attacker picks a target account and a list — a dictionary of common passwords, leaked password lists, or generated combinations — and a tool submits each one to the login endpoint as fast as the server will answer. Because the whole thing is automated, speed is everything: the more guesses per second the server accepts, the sooner the attacker wins. Anything that slows the guessing rate, from a per-account limit to a CAPTCHA, attacks the economics directly.

Brute force targets one account with many passwords. Its cousin, credential stuffing, does the opposite — many accounts, one stolen password each — and needs a slightly different defence, covered in the credential-stuffing prevention guide.

The defences, in order

  • Rate-limit failed attempts. Return 429 after 5–10 failures per account or per IP, with a Retry-After cool-down that grows on repeat abuse.
  • Add a CAPTCHA after a few failures. A Turnstile or hCaptcha challenge after the second or third miss breaks automation while letting a real user through.
  • Require multi-factor authentication. Even a correctly guessed password does not grant a session without the second factor — the strongest backstop.
  • Never reveal which field was wrong. Return the same generic “invalid username or password” whether the username exists or not, so the attacker cannot enumerate valid accounts.
  • Throttle at the edge. A WAF (Cloudflare, AWS WAF) absorbs high-volume bursts before they reach your app.
Common gotcha: a permanent account lockout after N failures looks safer but hands attackers a weapon — anyone who knows a username can lock the real owner out on purpose, a denial-of-service. Prefer a temporary cool-down, exponential back-off, or a CAPTCHA over a hard, sticky lock.

Developer: exact syntax

Enforce the limit on the endpoint that actually checks the password — the API route, not only the HTML form — so a bot posting straight to the JSON endpoint cannot bypass it.

Node / Express — express-rate-limit

import rateLimit from "express-rate-limit";

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 10,                // per IP per window
  standardHeaders: true,    // includes Retry-After
  legacyHeaders: false,
});

app.post("/api/auth/login", loginLimiter, loginHandler);

Django — django-axes

# settings.py
INSTALLED_APPS += ["axes"]
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1  # hours
AXES_LOCKOUT_PARAMETERS = ["ip_address", "username"]

Java / Spring Security — attempt tracking

@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.

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 returns 429 automatically when the throttle trips.

Raw HTTP — the response to return

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"error": "too_many_requests", "retry_after": 60}

For what the 429 status and Retry-After header mean in detail, see the HTTP 429 guide. The OWASP Authentication Cheat Sheet is the reference for the wider set of login protections.

Test whether your login page pushes back on repeated failures — free, no signup.

Run the login rate limit checker

This is one part of the bigger picture — see the pillar, how to rate-limit your login page, which ties brute force, credential stuffing, and the 429 signal together.

Frequently asked questions

How do I stop brute force login attacks?

Rate-limit the login endpoint so that after roughly 5 to 10 failed attempts against an account or from an IP address, the server returns HTTP 429 Too Many Requests with a Retry-After header instead of checking the password again. This makes automated guessing far too slow to work. Layer additional defences on top: present a CAPTCHA after the second or third failure to break automation without blocking real users, require multi-factor authentication so a correct guessed password still cannot grant a session, and return an identical generic error whether or not the username exists so attackers cannot enumerate valid accounts. Enforce the limit on the API endpoint that actually verifies the password, not only on the HTML login form, or a bot posting directly to the JSON endpoint will bypass it. Prefer a temporary cool-down or exponential back-off over a permanent lockout, and confirm the protection works by testing your own login page.

How many failed attempts before I lock an account?

A reasonable default is to start slowing or challenging a login after 5 to 10 consecutive failures for the same account, with a broader per-IP ceiling to catch a single source attacking many accounts. But the more important choice is what happens at the threshold. A permanent account lockout is risky: anyone who knows a username can deliberately trigger it and lock out the real owner, which turns your defence into a denial-of-service tool against your own users. Instead, use a temporary cool-down that grows with exponential back-off each time the limit is breached again, or present a CAPTCHA challenge, so a real user can recover quickly while an automated attacker faces ever-increasing delay. Tune the exact numbers to your traffic after deploying, and monitor how often real users hit the limit so you can loosen or tighten it based on evidence rather than guesswork.

Do I still need MFA if I have rate limiting?

Yes — they defend against different failure modes and are strongest together. Rate limiting slows down or blocks automated guessing, but it does not help if an attacker already has the correct password, whether they guessed a weak one within the limit, phished it, or replayed it from another site's breach. Multi-factor authentication closes that gap: even a completely correct password cannot complete a login without the second factor, such as an authenticator app code or a passkey. Conversely, MFA alone still benefits from rate limiting, because unlimited attempts let attackers grind against the login endpoint and probe for weaknesses, and can generate a flood of MFA prompts to the real user (an MFA-fatigue attack). Deploy rate limiting as the front-line control that makes automation uneconomical, and MFA as the backstop that makes a stolen or guessed password insufficient on its own. Together they cover both brute force and credential stuffing.

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 →