Group
Stability2 min read

Rate Limiting for People Who've Never Heard of It (Part 3)

A gentle, practical introduction to rate limiting for developers who shipped an app before they knew what rate limiting was.

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:

  1. 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.
  2. Abuse control. Signup endpoints, contact forms, password reset — without limits, they're spam vectors.
  3. 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.

Get an expert audit of your vibe coded app

Don't go live without letting an expert set of eyes review your code. Fortivibe runs 173 checks against your repo, reviewing things like security, stability, and user experience.

Get Your App Audited Now

Team Fortivibe Updated 32 days ago

Copy Link