HomeProjectsBlogContact

API Rate Limiting: Protect Your Backend from Abuse and Surprise Bills

One script hammering your login endpoint, or one user looping your AI feature, can take down a server or empty a budget. How rate limiting works, which algorithm to pick, and how to add it.

API Rate Limiting: Protect Your Backend from Abuse and Surprise Bills

Every public endpoint will eventually get hit far harder than you planned. Sometimes it's an attacker trying thousands of passwords. Sometimes it's a scraper. Sometimes it's your own frontend stuck in a retry loop. And if your endpoint calls a paid API, an AI model for example, every one of those requests costs you money.

Rate limiting is the seatbelt for all of this. Here's how it works and how to add it without annoying real users.

What rate limiting does

A rate limiter tracks how many requests a client makes in a time window and rejects requests over the limit, usually with HTTP 429 Too Many Requests.

The three decisions you need to make:

  1. Who is the client? (the key)
  2. How many requests per how long? (the limit)
  3. Which algorithm counts them?

Choosing the key

  • IP address: works for anonymous traffic. Weaknesses: many users can share one IP (offices, mobile carriers), and attackers can rotate IPs.
  • User ID: best for authenticated endpoints. Fair per person, regardless of network.
  • API key: for public APIs and integrations, limits per customer or plan.
  • Combinations: for login, limit by IP and by target account, so a single attacker can't hammer one account, and one account can't be attacked from many IPs too quickly.

If you're behind a proxy or CDN, make sure you read the real client IP from the trusted forwarded header, not the proxy's IP. Otherwise every request appears to come from the same address.

The common algorithms

Fixed window. Count requests per calendar window, like "100 per minute starting at :00." Simple and cheap. The weakness: a burst at the end of one window and the start of the next can allow double the limit in a short span.

Sliding window. Counts requests in the last N seconds from now, smoothing out the boundary problem. A very good default for most APIs.

Token bucket. Each client has a bucket that refills at a steady rate, up to a maximum. Each request takes a token. It allows short bursts (a page load firing several requests) while enforcing a steady average. Widely used for public APIs.

Leaky bucket. Requests queue and are processed at a fixed rate. Good for smoothing traffic to a fragile downstream system.

If you're unsure, use a sliding window for simple per-endpoint limits and a token bucket where bursts are normal.

Where to put the limiter

  • At the edge (CDN or WAF): stops abusive traffic before it reaches your servers at all. Many CDNs offer rate limiting rules.
  • API gateway or reverse proxy: Nginx, for example, has built-in rate limiting modules.
  • In your application: the most flexible, since you know the user, their plan and which endpoint is expensive.

Layering works well: coarse IP limits at the edge, precise per-user limits in the app.

Storage: why memory isn't enough

An in-memory counter works on one server. With several instances, or serverless functions that start fresh, each one counts separately and the limit stops meaning anything.

Use a shared store, typically Redis. Its atomic increment and expiry commands are perfect for counters. Hosted Redis with HTTP access (like Upstash) suits serverless platforms, and there are ready-made rate limiting libraries built on it.

In Express, express-rate-limit is a common starting point, with stores available for Redis:

import rateLimit from 'express-rate-limit'

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,   // 15 minutes
  limit: 10,                  // 10 attempts per window
  standardHeaders: true,      // send RateLimit headers
  legacyHeaders: false,
})

app.post('/api/login', loginLimiter, loginHandler)

Respond helpfully

When you reject a request:

  • Return 429 Too Many Requests.
  • Include a Retry-After header with how many seconds to wait.
  • Optionally send rate limit headers showing the limit, the remaining count and the reset time, so well-behaved clients can slow down on their own.
  • Use your normal error format (consistent API errors) with a clear message.

On the client side, handle 429 gracefully: back off, retry later with exponential backoff and some random jitter, and show a friendly message instead of a broken page.

Endpoints that need limits first

If you can only protect a few, start here:

  1. Login, signup, password reset and OTP verification: brute force and credential stuffing targets.
  2. Anything that costs money per call: AI model APIs, SMS, email, payment lookups, paid third-party APIs. (Keeping AI apps affordable.)
  3. Expensive queries: search, exports, reports.
  4. File uploads. (Upload security guide.)
  5. Public forms: contact and comment forms that attract spam.

For cost-sensitive endpoints, add a daily or monthly quota per user on top of short-term rate limits, plus spending caps in the provider's dashboard.

Picking the numbers

There's no universal right limit. A practical approach:

  • Look at real usage. What does a heavy but legitimate user do?
  • Set the limit comfortably above that, often several times higher.
  • Start in log-only mode: record who would have been limited without blocking anyone.
  • Tighten gradually, and give paying or trusted users higher limits.

Sensitive endpoints like login deserve much stricter limits than ordinary reads.

Rate limiting isn't the whole defence

It slows attackers down; it doesn't replace authentication, input validation or proper access control. Combine it with CAPTCHA on suspicious signups, account lockout policies and monitoring for unusual spikes. The web application security checklist covers the rest.

FAQ

What is a good rate limit for an API?

It depends on the endpoint. Base it on real usage: set limits well above what legitimate heavy users need, be much stricter on login and costly endpoints, and adjust using logs.

What HTTP status code should a rate limit return?

429 Too Many Requests, ideally with a Retry-After header telling the client how long to wait.

Should I rate limit by IP or by user?

Both, depending on the endpoint. Use IP limits for anonymous traffic and at the edge, and per-user or per-API-key limits for authenticated endpoints, where they're fairer and harder to evade.

Keep reading

More from the workbench

All articles →