API Protection

Rate Limiting

Rate limiting protects an API from abuse, bugs and runaway clients. A few algorithms and clear headers keep your service available for everyone.

intermediate14 min readUpdated Sep 15, 2026
limit.js
js
// limit.js
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, "1 m"),
});

export async function handler(req, res) {
  const id = req.headers["x-api-key"] ?? req.ip;
  const { success, remaining, reset } = await ratelimit.limit(id);

  res.set("RateLimit-Remaining", String(remaining));
  res.set("RateLimit-Reset", String(Math.ceil(reset / 1000)));

  if (!success) {
    res.set("Retry-After", "60");
    return res.status(429).json({ error: "rate_limited" });
  }
  // ...handle the request
}
Status
429 Too Many Requests
Hint
Retry-After header
Identity
Key, user or IP
Algorithms
Window, bucket
Storage
Redis for many instances
Goal
Availability for everyone

Why it matters

Why rate limit

Protect availability

A single misbehaving client or a scraping bot can exhaust your capacity. Limits keep the service up for everyone else.

Fair usage

Per-key and per-tier limits ensure one caller cannot consume more than its share.

Bound the damage

Limits also cap the cost of bugs, such as a client stuck in a retry loop hammering your API.

The big picture

The three ideas behind rate limiting

Identify the client, count its requests with an algorithm, and tell it what happened with the right status and headers.

Identity

Count

Decide what a request is counted against — an API key, a user, an IP or a combination.

Algorithm

Limit

Fixed window, sliding window, token bucket or leaky bucket decide how requests are allowed.

Response

Signal

Return 429 with Retry-After and rate limit headers so clients can back off correctly.

Rate limiting at a glance

The core ideas

Fixed window

A simple counter reset every interval; cheap but allows bursts at the boundary.

Sliding window

Smooths the boundary problem with better accuracy.

Token bucket

Allows bursts up to a bucket size while enforcing an average rate.

Leaky bucket

Processes at a steady rate and queues or drops the excess.

429 and Retry-After

The standard way to say "slow down, try again later".

Distributed state

Share counters in Redis so limits apply across every instance.

A short history

From IP blocks to distributed limiters

  1. 2000s

    IP-based blocking

    Early defenses block abusive IPs after the fact.

    2000s
  2. 2010s

    API keys and quotas

    Public APIs introduce per-key limits and monthly quotas.

    2010s
  3. 2015

    Redis limiters

    Shared counters make limiting work across many servers.

    15
  4. 2020

    Standard headers

    RateLimit headers are proposed to make limits discoverable.

    20
  5. Today

    Layered defenses

    Limits, quotas, WAFs and bot detection work together.

    Today

The complete guide

Rate Limiting: Everything you need to know

Why rate limit

Every API has finite capacity, and not every caller is well behaved. A scraper, a buggy client stuck in a retry loop, or a burst of legitimate traffic can exhaust your database connections and take the service down for everyone. Rate limiting caps how fast a client may make requests so no single caller can consume the whole system.

It also makes costs predictable, enforces fair use across tenants and plans, and gives you a lever to slow abuse before it becomes an outage.

What to limit

A limit is only meaningful relative to an identity. Common choices:

  • API key — the best option for server-to-server and third-party clients.
  • User id — natural for authenticated apps.
  • IP address — a fallback for anonymous traffic, but many users can share one.
  • Combination — key plus IP, or user plus endpoint, for precision.
  • Endpoint or tier — stricter limits on expensive operations like search or exports.

Also decide the scope: a global limit, a per-endpoint limit, or both. A global limit protects the service; per-endpoint limits protect specific expensive work.

Algorithms

Four algorithms cover almost every need.

Fixed window — count requests in a fixed interval and reset at the boundary.

limit: 100 per minute
key: client:123:2026-09-15T10:05

It is cheap and simple, but a client can send 100 requests at the end of one window and 100 at the start of the next, effectively doubling the rate at the boundary.

Sliding window — count over the last N seconds rather than a fixed block, usually by combining the current and previous window with a weighted average. Smoother than fixed window with similar cost.

Token bucket — a bucket refills at a steady rate, and each request consumes a token. It allows short bursts up to the bucket size while enforcing an average rate, which matches how real clients behave.

// token-bucket.js
const capacity = 20;
const refillPerSecond = 5;
let tokens = capacity;
let last = Date.now();

function allow() {
  const now = Date.now();
  tokens = Math.min(capacity, tokens + ((now - last) / 1000) * refillPerSecond);
  last = now;
  if (tokens < 1) return false;
  tokens -= 1;
  return true;
}

Leaky bucket — requests enter a queue that drains at a fixed rate. It smooths traffic to a constant output, useful when downstream systems need steady load.

Responding to limits

Tell clients what is happening with standard signals.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
  • 429 Too Many Requests is the correct status code.
  • Retry-After tells the client when to try again, in seconds or a date.
  • RateLimit-* headers expose the limit, remaining count and reset time so clients can pace themselves.

Returning 200 or silently dropping requests hides the problem and leads to mysterious client bugs. Well-behaved clients read Retry-After and back off automatically.

Distributed limiting

If you run more than one instance, counters must be shared. In-memory limits apply per instance, so with ten servers a client effectively gets ten times the limit.

// redis.js
const key = `ratelimit:${apiKey}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);

if (count > 100) {
  // reject with 429
}

Use atomic operations, or a library that uses a Lua script, so increments and expiries are race-free. Redis is the usual store because it is fast and supports atomic scripts and expiry. API gateways and edge platforms can enforce limits before traffic even reaches your application, which is the most efficient place to do it.

Quotas versus rate limits

They solve different problems and often coexist.

  • Rate limit — how fast: 100 requests per minute.
  • Quota — how much: 100,000 requests per month, tied to a plan.

A client can stay under its rate limit all month and still exhaust its quota. Track quotas separately, usually with a longer-lived counter, and return a distinct error when a quota is exhausted so clients know the difference.

Avoiding harm to real users

Limits should stop abuse without punishing normal usage.

  • Set limits from measured traffic, with headroom for bursts.
  • Allow bursts with a token bucket rather than a hard cliff.
  • Use different limits per tier and per endpoint.
  • Exclude health checks, internal calls and static assets.
  • Prefer temporary slowdowns to permanent bans.
  • Monitor rejection rates and tune; a spike in 429s may mean a limit is too tight.

Best practices

  • Identify clients with the most specific stable key available.
  • Choose an algorithm that matches the traffic shape.
  • Return 429 with Retry-After and rate limit headers.
  • Share counters in Redis or enforce limits at the edge.
  • Separate rate limits from quotas and expose both.
  • Allow bursts and set limits from real measurements.
  • Log and monitor rejections, and alert on sudden spikes.

Common mistakes

  • Counting per instance and multiplying the effective limit.
  • Using only IP, which punishes users behind shared NAT.
  • Returning 200 or dropping requests silently.
  • Setting limits so tight that normal clients are blocked.
  • Forgetting to expire counters and leaking memory.
  • Treating rate limits as a substitute for authentication and authorization.

Where to go next

Rate limiting keeps an API available under pressure. Ground it in REST design and the HTTP guide, pair it with API Versioning as part of the lifecycle, and implement it on Node.js. Then add a limiter to one endpoint and watch how it behaves under a load test.

Rejecting a request

Return 429 with a Retry-After hint and remaining count. Silently dropping or returning 200 confuses clients and hides the problem.

Prefer
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Avoid
HTTP/1.1 200 OK
# silently ignored or
# returns an empty body

Storing counters

With multiple instances, counters must be shared. In-memory limits apply per instance and let clients get N times the intended rate.

Prefer
const { success } = await ratelimit.limit(key);
// shared across every instance
Avoid
const counts = new Map();
// each server has its own map,
// so the real limit is N x

Trade-offs

Is rate limiting the right defense?

Limits protect availability and bound abuse, but they add state and can punish legitimate users if the identity or thresholds are wrong.

Strengths

  • Keeps the service up

    One runaway client or scraper cannot exhaust capacity, so everyone else keeps getting served.

  • Fair sharing

    Per-key and per-tier limits stop a single caller from consuming more than its share of a shared resource.

  • Caps the cost of bugs

    A client stuck in a retry loop is contained before it turns into an outage or a large bill.

Trade-offs

  • Needs shared state

    Correct limits across instances require Redis or a gateway, which adds a dependency on the hot path.

  • Easy to get wrong

    Too-tight limits or the wrong identity reject real users, and IP-based limits punish everyone behind a NAT.

  • Not a full defense

    Limiting alone does not stop distributed attacks, so it works best alongside quotas, WAFs and bot detection.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Rate Limiting?

Our interactive tutorial walks you through Rate Limiting step by step — with quizzes and real code you can run in the browser.