Routes that need limits first
- Login and password reset, to slow guessing.
- OTP send and verify, to stop SMS pumping and code guessing.
- Signup and contact forms, to cut spam.
- Any route that calls a paid API such as an LLM, email or SMS provider.
- Search or export endpoints that are expensive to compute.
Pick keys carefully
An IP-only limit is easy to bypass and can block many users behind one address. Combine IP with the account, email or phone number under attack, and use a shorter window for failed attempts.
A simple fixed-window limiter
This sketch uses a key-value store with expiry. Production code should use an atomic increment.
const key = `rl:${route}:${identity}:${Math.floor(Date.now() / windowMs)}`;
const count = await store.incr(key);
if (count === 1) await store.expire(key, windowSeconds);
if (count > limit) {
return new Response('Too many requests', {
status: 429,
headers: { 'Retry-After': String(windowSeconds) },
});
}Also limit at the edge
Your host or CDN can throttle traffic before it reaches your code. Use that for blunt protection and keep account-aware limits in the app.
Frequently asked questions
What limit should I choose?
Start strict for sensitive routes, for example a handful of login attempts per account per few minutes, then loosen based on real usage.
Is a CAPTCHA a replacement?
It helps against bots on forms but does not cap volume. Use it together with limits.