Rate limiting for APIs and forms: how we set the limits
How we decide the numbers behind rate limits on forms, logins and APIs, what happens when they are hit, and how we tune them from real traffic.
The short answer
Rate limits are only useful if the numbers are right: too tight and real users are blocked, too loose and abuse walks through. We set them per endpoint, because a login, a form and a search have different genuine patterns; per source, so one abuser does not affect anyone else; and per account or per key where the endpoint has one, so attacks on a single user or a single integration are contained. The starting numbers come from real usage in the logs, with a wide margin above the busiest genuine pattern, and are tightened only with evidence. Logins are slowed rather than locked, so attackers are throttled without being handed a way to lock users out. APIs get per-key quotas matched to the plan or the integration, plus global ceilings that bound cost. Every limit returns a clear response with a retry time and is logged, so genuine users understand what happened and abuse patterns are visible.
Limits by endpoint type
| Endpoint | Dimensions | Starting shape | Behaviour when hit |
|---|---|---|---|
| Contact and quote forms | Per source; global daily | A few per minute per source, several times the genuine peak; a daily ceiling | Clear message, retry time, input preserved |
| Login | Per account; per source | A handful of failures, then increasing delay; source limited after more | Slowing, then limit; owner notified of repeated failures |
| Password reset | Per account; per source | Low, because genuine use is rare | Message says an email was sent if the account exists |
| Search | Per source | Tens per minute | Message with retry |
| Public API | Per key; per source; global | Quota matching the plan; burst allowance; global ceiling | Status code, remaining allowance and reset headers |
| Webhook receivers | Per sender; global | Generous, because senders retry | Accept and queue; reject only floods |
| Expensive functions | Per source; global | Tight per source; global ceiling for cost | Message; alert to the team when the ceiling is approached |
| Newsletter sign-up | Per source; per address | Low | Message; confirmation flow unchanged |
How we tune them
- Measure genuine usage per endpoint from a few weeks of logs.
- Set limits several times above the genuine peak, per source, per account, per key.
- Choose the behaviour: slowing for logins, clear rejection for forms, headers for APIs.
- Add signals rather than tightening blindly: bot checks on forms, honeypots, reputation from the network layer.
- Log every limit event with endpoint, dimension and source.
- Review weekly at first, then monthly: raise where genuine users were hit, add signals where abuse got through.
- Set global ceilings on cost-bearing endpoints so the bill has a bound no matter what.
Limits and the rest of the defence
Rate limits work with, not instead of, the other layers: the network layer in front filtering known bad traffic, bot checks on forms, authentication on APIs, input validation everywhere, and monitoring that notices patterns. A limit is the backstop that bounds what any single source can do when the other layers let something through, and its log is where new abuse patterns first appear.
What this means for you
Good rate limits are per endpoint, per source and per account or key, set from real usage with a wide margin, tuned with evidence, slowing rather than locking on logins, bounded globally on anything that costs money, and always visible when they fire. Set that way, they stop abuse without ever being noticed by a genuine user, and their log becomes one of your most useful security signals.
Frequently asked questions
How do you decide the actual numbers?
From traffic, once there is traffic to read. The method: take the busiest genuine pattern for an endpoint over a few weeks, such as the most form submissions a real visitor made in a minute, and set the limit several times higher. Until those measurements exist, a limit is a generous default rather than a derived number, and that is what ours is today: it sits high enough that a real visitor will not meet it. The number becomes evidence the moment someone reads the traffic and adjusts it.
Why not lock accounts after failed logins?
Because an attacker can then lock any user out by failing on purpose, which is a denial of service you built yourself. Instead we slow down: after a few failures, each attempt waits longer, and after more, the source is limited and the account owner is notified. Attackers are throttled to uselessness; genuine users who mistype twice barely notice.
What does a user see when they hit a limit?
A plain message saying too many requests were made and when to try again, with what they typed preserved, and a proper status code so any automated client understands. For APIs, headers tell the client its remaining allowance and when it resets, so well-behaved integrations back off on their own.
Sources
- OWASP API Security Top 10: Unrestricted Resource Consumption (accessed 2026-09-14)