Most sites rate-limit the whole API uniformly at the edge. That’s fine for preventing DDoS, useless for stopping credential stuffing. Auth endpoints need their own, sharper controls.
Why IP-based limits fail
Credential stuffing attacks don’t come from a single IP. They come from botnets of residential proxies — 10,000 compromised home routers in 50 countries, each making one or two attempts per hour. A 100-request-per-minute IP limit never triggers.
What triggers is the attacker’s pattern: one account, many attempts. That’s the signal to catch.
The design
Per-account rate limit on login:
- 5 failed attempts in 5 minutes → lock the account for 15 minutes
- 5 more failed attempts after unlock → lock for 1 hour
- 5 more → lock for 24 hours, notify the account owner via email
The escalation matters. A one-time lockout is easily brute-forced around by a patient attacker. Exponential cooldowns with email notification turn a credential stuffing attack into a very visible, very slow denial-of-service that admins can investigate.
Per-IP rate limit as a secondary signal:
- 100 login attempts per IP per minute → short block
- This catches dumber attacks from single-IP bots
Per-account rate limit on password reset and MFA:
- Same pattern — 5 attempts in 5 minutes, then escalating cooldowns
- Password reset brute force is a real attack (guessing short reset tokens)
- MFA brute force is what happens after a password compromise — it’s your last line of defense
Implementation
In Redis or Postgres, keep a counter per (account_id, endpoint, time_bucket). On each attempt, increment. If the counter exceeds the threshold, reject with a 429 and a Retry-After header. Reset or decay the counter over the window.
Capture the failed attempts in your audit log, not just in the rate-limiter state. You want to be able to answer “how many failed attempts did account X have last week?” in 30 seconds.
Captcha as a fallback
For public-facing consumer apps, add a Captcha on the 3rd or 4th failed attempt from a given account or IP. It’s annoying, but so is a successful account takeover. Users who got their own password wrong once won’t see it; attackers will see it constantly.
What not to do
Don’t rate-limit so aggressively that a legitimate user who typos three times gets locked out for an hour. 5 attempts in 5 minutes with a 15-minute cooldown is the sweet spot for most consumer and B2B apps. Tune it to your user base.