Rate limiting and defending against brute force and credential stuffing

Attackers automate guessing. Rate limits, smart lockouts and multi-factor authentication make that guessing slow, costly and noisy.

A gate limiting how many requests pass per minute

Anything an attacker can try automatically, they will try thousands of times: passwords, one-time codes, coupon codes, reset tokens, API endpoints. Rate limiting controls how often a client can do something, so abuse becomes slow and expensive while normal users are not affected. It also protects against accidental overload and runaway scripts.

This is a defensive guide to designing limits and related controls.

The threats

  • Brute force: many guesses against one account (for example all passwords or all 6-digit codes).
  • Password spraying: a few common passwords tried against many accounts, staying under per-account limits.
  • Credential stuffing: using username and password pairs leaked from other sites, since many people reuse passwords. Often done with large botnets and many IP addresses.
  • Enumeration: discovering which emails or usernames exist from different responses.
  • Abuse of expensive endpoints: search, exports, password reset emails, SMS sending (which costs money), and heavy API calls.
  • Denial of service from excessive requests.

Rate limiting algorithms

  • Fixed window: allow N requests per time window (for example 100 per minute). Simple, but a burst at the window boundary can allow up to double the limit.
  • Sliding window: counts requests over a rolling period, smoother and fairer.
  • Token bucket: a bucket holds tokens that refill at a steady rate. Each request uses a token. It allows short bursts while enforcing an average rate, and it is widely used.
  • Leaky bucket: requests are processed at a constant rate, smoothing bursts.
  • Concurrency limits: cap the number of requests in flight at one time.

A shared store such as Redis is commonly used to keep counters consistent across several servers. Make the update atomic (for example with a Lua script or atomic increment with expiry) so concurrent requests cannot slip past.

What to limit by

No single key is enough. Combine several:

  • IP address: simple, but many users can share one IP (offices, mobile carriers), and attackers can rotate IPs. Be careful to use the real client IP from a trusted proxy header and not a value the client can forge.
  • Account or username: limits guessing against one target even from many IPs.
  • API key or user ID for authenticated APIs.
  • Device or session fingerprints as an additional signal.
  • Endpoint-specific limits: stricter for login, password reset, OTP verification and SMS sending than for harmless reads.

Responding when the limit is hit

  • Return HTTP 429 Too Many Requests with a Retry-After header, and a clear message.
  • Use progressive delays (each failed attempt waits longer) or temporary lockouts of minutes rather than long permanent ones.
  • Beware that hard account lockouts let an attacker lock real users out on purpose (a denial of service). Prefer soft measures: slowing down, requiring a CAPTCHA, or requiring extra verification from unknown devices, while allowing the known device or a verified reset path.
  • Do not reveal too much detail about limits to attackers. Keep rules and thresholds sensible.

Login-specific protections

  • Generic error messages: "Incorrect email or password", the same for unknown users and wrong passwords. Keep response time similar, since timing differences can also leak which accounts exist.
  • Apply limits on both IP and account, and monitor for the pattern of one IP trying many usernames.
  • Multi-factor authentication (MFA) is the strongest defence against credential stuffing, because stolen passwords alone no longer work. Prefer authenticator apps, security keys or passkeys over SMS where possible.
  • Check passwords against known breached lists when users set them, and alert or force a reset for accounts using exposed passwords.
  • Use strong password hashing (see the password storage article) so a database leak is less useful.
  • Notify users of new-device logins and suspicious attempts.
  • Consider risk-based authentication: ask for extra proof when the device, location or behaviour is unusual.

CAPTCHA and bot management

  • A CAPTCHA or challenge can slow automated attacks, best used after suspicious behaviour rather than for everyone, because it hurts usability and accessibility.
  • Bot management services (often part of a CDN or WAF) use reputation, behaviour and fingerprints to filter traffic, and can block known bad networks.
  • No single challenge is perfect, since attackers can use solving services, so layer controls.

OTP and reset token protection

  • Limit verification attempts for one-time codes. A 6-digit code has only a million possibilities, so without limits it can be brute forced.
  • Make codes and reset tokens short-lived, single-use and random.
  • Rate limit sending of SMS and email to prevent cost abuse and harassment of victims.
  • Invalidate previous codes when a new one is issued.

Protect expensive and sensitive endpoints

  • Stricter limits on search, exports, file processing and anything calling a paid service.
  • Request size and timeout limits, pagination caps and query complexity limits.
  • Use quotas per plan or user for APIs (daily or monthly), plus a short-term rate limit.
  • Queue heavy work and process it asynchronously.

Put limits in the right layer

  • Edge (CDN, WAF, API gateway): blunt, early filtering of floods and obvious bots before they reach your servers.
  • Application: fine-grained rules that understand users, accounts and business logic (for example per-account login attempts).
  • Both layers together work best.

Monitoring and detection

  • Log failed logins, lockouts and rate-limit hits with IP, account, user agent and endpoint (without logging passwords).
  • Alert on spikes: failed-login rate, many accounts from one IP or the reverse, new-device logins at unusual volume, high 429 counts.
  • Review logs after incidents and tune thresholds. Too strict harms real users, too loose does nothing. Test with realistic traffic.

Common mistakes

  • Trusting the X-Forwarded-For header without checking that it comes from your own proxy, so attackers can fake their IP and bypass limits.
  • Limiting only by IP, or only by account.
  • Counters that are not atomic or are kept in the memory of a single server behind a load balancer.
  • Different error messages that reveal valid accounts.
  • Permanent hard lockouts that attackers can abuse.
  • No limit on OTP verification or password reset requests.
  • No monitoring, so attacks go unnoticed.

Checklist

  1. Limit login, reset, OTP and signup endpoints by IP and account.
  2. Use token bucket or sliding window with an atomic shared store.
  3. Return 429 with Retry-After, and use soft, temporary penalties.
  4. Offer MFA, check breached passwords, notify on new devices.
  5. Add CAPTCHA or challenges after suspicious activity.
  6. Limit OTP attempts and rate limit SMS and email sending.
  7. Protect expensive endpoints with quotas.
  8. Log, alert and tune.

Takeaway: assume automated guessing will happen. Combine rate limits by IP and account, MFA, breached-password checks, soft lockouts, bot controls and monitoring, so attacks are slow, noisy and unprofitable.


Web security series, Part 3 (accounts and defence): 5 of 5. Previous: Dependency and supply chain security: protecting your app from risky packages.

Keep reading