Rate limiting is unglamorous and disproportionately effective. It blunts credential stuffing, stops contact forms being used as spam relays, prevents costly endpoints being hammered, and buys you time when something starts going wrong.

What to limit, and how hard

  • Sign-in: a handful of attempts per account per hour, and a separate ceiling per network address.
  • Password reset and one-time codes: tighter still, since the cost of abuse is account takeover.
  • Registration: constrained enough that automated account creation is not worth the effort.
  • Contact and intake forms: low limits, because volume here is almost always abuse.
  • Expensive operations — search, exports, AI calls, file processing: limited by cost, not just by count.

Limit on two axes

Limiting by account alone lets an attacker spread attempts across many accounts from one machine. Limiting by address alone is defeated by rotating addresses and punishes shared networks. Apply both, with different thresholds, and treat the account limit as the stricter one.

Behaviour when limited

Return HTTP 429 with a Retry-After header so legitimate clients can back off correctly. Keep the message identical regardless of whether the account exists — a reset endpoint that rate-limits differently for real and fake addresses leaks which are real. And make sure the limit applies server-side; anything enforced only in the browser is not a limit.

Watch the rejections

Rejected requests are a signal, not noise. A sudden rise in 429s on a sign-in route is a credential-stuffing run starting. Log the endpoint, the count and the coarse source, alert when the rate jumps sharply, and you will often know about an attack before anything else tells you.