Rate limiting explained: protect your API from abuse and overload
Mosaddik Billah 5 min read

On this page
A rate limit caps how many requests one client can make in a period of time: say, 5 login attempts a minute, or 100 API calls a minute per key. Without one, a single script can guess passwords all night, scrape your whole catalogue, or run up your AI bill, and one busy client can slow the app down for everyone else.
What to limit, and by what#
Two choices define a limit:
- The key: who counts as “one client”. Usually the API key or user ID when the request is signed in, and the IP address when it isn’t. IPs are shared (an office, a mobile network) and easy to change, so limits by IP should be generous.
- The scope: which requests count. A limit on everything catches floods; a much tighter limit on sensitive or expensive endpoints catches targeted abuse.
Good places for tight limits:
- Login, sign-up and password reset: stops password guessing and email bombing.
- Anything that costs you money per call: sending SMS or email, calling an AI model. See AI tokens and costs.
- Expensive searches and exports.
Fixed window: the simple one#
Count requests per key in a time window, such as each minute. Over the limit, refuse until the window resets:
// At most `limit` requests per `windowMs` for each IP.
function rateLimit({ limit, windowMs }) {
const hits = new Map();
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
let entry = hits.get(key);
if (!entry || entry.resetAt <= now) {
entry = { count: 0, resetAt: now + windowMs };
hits.set(key, entry);
}
entry.count++;
if (entry.count > limit) {
res.set("Retry-After", String(Math.ceil((entry.resetAt - now) / 1000)));
return res.status(429).json({ error: "Too many requests. Try again later." });
}
next();
};
}
app.post("/login", rateLimit({ limit: 5, windowMs: 60_000 }), handleLogin);With a limit of 3, five quick requests get 200, 200, 200, 429, 429. The 429 Too Many Requests status and the Retry-After header (in seconds) tell well-behaved clients exactly when to try again.
Two things to know about this simple version:
- Bursts at the edges. A client can send the full limit at 0:59 and again at 1:00, so twice the limit in two seconds. Usually fine; when it isn’t, use a token bucket.
- The memory is per server. It resets on restart and isn’t shared if you run several servers. For production, store the counters in Redis, or use a tested package such as
express-rate-limitwith a Redis store.
If your app runs behind a proxy or load balancer, req.ip may be the proxy’s address. In Express, set app.set("trust proxy", 1) (or the right number of proxies) so it reads the real client IP.
Token bucket: smooth limits with bursts#
Picture a bucket that holds, say, 5 tokens and refills at 1 token a second. Each request takes a token; an empty bucket means wait. Clients can burst up to the bucket size, then settle to the refill rate.
class TokenBucket {
constructor(capacity, refillPerSecond) {
this.capacity = capacity;
this.tokens = capacity;
this.refillPerSecond = refillPerSecond;
this.last = Date.now();
}
take() {
const now = Date.now();
const refill = ((now - this.last) / 1000) * this.refillPerSecond;
this.tokens = Math.min(this.capacity, this.tokens + refill);
this.last = now;
if (this.tokens < 1) return false;
this.tokens -= 1;
return true;
}
}A new bucket of 5 allows 5 requests at once, then refuses. Two seconds later, two more are allowed. You keep one bucket per key. This is the model many large APIs use, because it allows short bursts while keeping the average rate fixed.
There’s also the sliding window, which counts requests in the last 60 seconds from now rather than since the start of the minute. It smooths out the edge bursts of a fixed window and is what many libraries offer.
Choosing the numbers#
- Start from real usage. Look at how many requests your heaviest genuine users make, and set the limit well above it.
- Be strict where it’s cheap for users. Nobody needs 50 password attempts a minute; 5 to 10 is plenty.
- Different tiers for different clients. Signed-in users and paying API customers can get more than anonymous traffic.
- Log every
429. A sudden rise tells you either someone is attacking, or your limit is too tight for a real user.
Rate limiting on the client side#
When you’re the one calling a rate-limited API:
- Read
429responses and theRetry-Afterheader instead of retrying immediately. - Back off exponentially (wait 1s, 2s, 4s…) with a little randomness, so many clients don’t retry in lockstep.
- Make retries safe with idempotency keys, so a retried
POSTdoesn’t create duplicates. - Spread bulk work out with a queue instead of sending everything at once.
Where to put it#
Rate limits can live in your app (as above), in an API gateway, or at the edge in a service such as Cloudflare or your hosting platform’s firewall. Edge limits stop floods before they reach your server at all; app limits know who the user is and what the request costs. Larger apps use both.
For the rest of the API, see REST API design best practices. If you need a public form, API or AI feature protected from abuse, I can set that up as part of AI integration and custom API integration.
Comments