If your app has been live for more than a week and you don't have rate limiting, one of two things is true: you have no users, or you're about to have a very expensive Tuesday.
What rate limiting actually is
Rate limiting is a rule that says "this identity can perform this action at most N times per T seconds." That's it. It is not a firewall, it is not authentication, and it is not a substitute for input validation. It is a knob on the volume of traffic a single caller can push through your app.
Why you need it
Three reasons, in order of how badly they'll ruin your week:
- Cost control. If you have an endpoint that calls OpenAI, and one caller can hit it 10,000 times an hour, you have handed a stranger the ability to run up your credit card.
- Abuse control. Signup endpoints, contact forms, password reset — without limits, they're spam vectors.
- Stability. One aggressive client can starve every other user of a shared resource (database connections, worker queue slots, egress bandwidth).
The four things you must rate limit
Even if you rate limit nothing else in your entire app, rate limit these:
- Login — per-IP and per-account, or someone will credential-stuff you.
- Signup — per-IP, or you'll wake up to 40,000 disposable accounts.
- Password reset — per-account, or attackers can grief legitimate users offline.
- Any endpoint that costs you real money per call — LLM calls, SMS, email, image generation.
A working example
Here is roughly what a real rate limiter looks like in production, using a token bucket in Redis:
const rate_limit = async ({ key, max_requests, window_seconds }) => {
const bucket_key = `rl:${key}`;
const current = await redis.incr(bucket_key);
if (current === 1) {
await redis.expire(bucket_key, window_seconds);
}
if (current > max_requests) {
const ttl = await redis.ttl(bucket_key);
throw { status: 429, retry_after_seconds: ttl };
}
};Call it from your route handlers with a key like login:${ip} or login:${email}.
What the failure looks like
Return a real 429 Too Many Requests with a Retry-After header. Don't return 500. Don't return 200 with an empty body. And log it — a spike in 429s is one of the best abuse signals you have.
What not to do
Do not implement rate limiting in-memory in a single Node process if your app scales to more than one instance. The limiter has to live in a shared store — Redis, Memcached, a database, anything — or half your traffic will bypass it.
Rate limiting is a five-line change that saves you thousands of dollars and hours of downtime. Do it before you launch, not after.