Part 7 · Reliability & Resilience

Rate Limiting Algorithms Explained

One buggy script calling 5,000 times a second can hurt every customer. How rate limiting protects your system and keeps usage fair: the five classic algorithms — with a simulator you can drive — distributed limiting in Redis, and how to tell clients to slow down.

Lesson 4312 min readintermediate#rate-limiting#api

On this page

  1. The Problem
  2. The Core Idea
  3. How It Works
  4. Trade-offs
  5. In the Real World
  6. Interview Questions
  7. Key Takeaways
  8. Further Reading

The Problem

Your public API (an interface that other programs call over the internet) is running well. Then:

  • A customer's script has a bug and calls GET /orders 5,000 times per second in a loop.
  • A bot (a program that acts like a user) tries millions of passwords on your login endpoint.
  • Someone scrapes your whole product catalogue every hour. (Scraping means copying data from a site with a program.)
  • A free-plan user's traffic slows the API down for paying customers.

Your servers are overloaded, the database is at 100%, and every customer suffers because of one client that behaves badly.

Rate limiting is a rule that controls how many requests a client can make in a given time. It protects your system, keeps usage fair between customers, controls costs, and blocks many kinds of abuse.

The Core Idea

Think about the entry gate at a busy metro station during rush hour.

  • The gates let people through at a steady rate that the platform can handle.
  • A short burst (a group of friends arriving together) is fine. But if thousands try to enter at once, guards hold people back so the platform does not overflow.
  • Everyone gets a fair chance.

A rate limiter is that gate. For each client (a user, an API key or an IP address), it keeps track of recent requests and decides:

  • Allow: the client is under the limit, so let the request pass.
  • Reject: the client is over the limit, so return 429 Too Many Requests (an HTTP status code) and say when to try again.

How It Works

What to limit, and where

Who is limited (the "key"): the key is the label that the limiter counts requests for.

  • per user or per API key (a secret code that identifies a customer's app): the most common choice for APIs where users log in,
  • per IP address: for traffic from users who are not logged in, like login pages and sign-up. (But many users can share one IP behind NAT, which lets many devices share one public address.)
  • per endpoint: stricter limits on expensive endpoints (search, exports) or sensitive ones (login, OTP, password reset),
  • per tenant or plan (a tenant is one customer account): "Free: 100 requests per minute; Pro: 1,000",
  • global: protect a weak downstream system (one that you call), for example "at most 500 calls per second to the SMS provider".

Where to enforce:

  • the API gateway or edge (post 13; a gateway is the single entry point in front of your services): it stops bad traffic early, before it costs anything,
  • the service itself: for limits that know your business ("5 OTP requests per phone number per hour"; an OTP is a one-time password),
  • the client: a well-made SDK (a code library for your API) can limit itself to avoid being rejected.

Algorithm 1: Fixed window counter

Count requests in fixed time windows (for example, each calendar minute). Set the count back to zero when the window changes. A window is a block of time.

Diagram
Limit: 100 requests per minute

12:00:00 ─────────────── 12:01:00 ─────────────── 12:02:00
   count: 0 → 1 → … → 100 ✅   count resets to 0
   request 101 → ❌ 429
Text
key = "rl:user42:" + current_minute   (e.g., rl:user42:202702251200)count = INCR keyif count == 1: EXPIRE key 60if count > 100: reject
  • ✅ Very simple and uses little memory (one counter per key per window).
  • ❌ The boundary burst problem. A client can send 100 requests at 12:00:59 and another 100 at 12:01:00. That is 200 requests in 2 seconds, double the intended rate.
allowedrejected (429)
tokens in bucket
5.0 / 5
+1 every second
allowed
0
rejected
0
clock
0.0 s

Bursts are allowed up to the bucket size, then traffic is smoothed to the refill rate. Most public APIs use this.

Three limiters, each allowing 5 requests per 5 seconds. Fire requests by hand — try a burst just before and just after a fixed-window boundary. · interactive

Algorithm 2: Sliding window log

Store the time of every request (a timestamp). For each new request, remove the timestamps that are older than the window, and count what is left. Allow the request if the count is under the limit.

Text
Limit 5 per 60 s. Log: [12:00:05, 12:00:20, 12:00:41, 12:00:50, 12:00:58]New request at 12:01:10 → drop 12:00:05 (older than 60 s) → 4 left → allow ✅, add 12:01:10

Redis is a fast in-memory data store. In Redis, a sorted set (a list kept in order by a score) works well. The score is the timestamp.

  • ✅ Exact. No boundary bursts.
  • ❌ Uses a lot of memory. It stores one entry per request. A limit of 10,000 per hour means up to 10,000 timestamps per client.

Algorithm 3: Sliding window counter (approximate)

A good middle way: keep counters for the current and the previous fixed window. Then weight the previous one by how much of it still overlaps the sliding window. (A sliding window is a window that moves with the clock, always covering the last 60 seconds.)

Text
estimated_count = current_window_count                + previous_window_count × (1 − elapsed_fraction_of_current_window)

Example: the limit is 100 per minute. The time is 12:01:15, which is 25% into the current minute. So 75% of the previous minute is still inside the sliding window.

  • Previous window (12:00–12:01): 80 requests.
  • Current window so far: 30 requests.
80
previous window
12:00–12:01
30
current window so far
12:01:00–12:01:15
30 + 80 × 0.75 = 90
weighted estimate
under 100 → allow ✅
The sliding window counter, worked through. Limit 100 per minute, 25% into the current minute. · numbers
  • ✅ Reduces boundary bursts, and uses only two counters per key.
  • ❌ Approximate. It assumes that requests in the previous window were spread evenly. In practice the result is very close.
  • It is widely used at large scale.

Picture a bucket that holds tokens. A token is a permission to make one request:

  • It has a maximum capacity (for example, 20 tokens), which is the burst size.
  • Tokens are added at a steady rate (for example, 10 per second), which is the sustained rate.
  • Each request takes one token. If the bucket is empty, the request is rejected (or waits).
  1. t = 0
    bucket full — 20 tokens
  2. burst
    20 requests arrive at once → all allowed; bucket empty
  3. next request, same instant
    no tokens → 429, Retry-After ≈ 0.1 s
  4. t = 0.5 s
    5 tokens refilled → 5 more requests allowed
A token bucket with capacity 20, refilling 10 tokens a second. · timeline

You do not need a timer that adds tokens. Just store tokens and last_refill_time. On each request, work out the new count:

Text
tokens = min(capacity, tokens + (now − last_refill_time) × refill_rate)if tokens >= 1: tokens -= 1 → allowelse: reject (and compute when the next token will arrive → Retry-After)
  • ✅ Allows short bursts (good for real users, who click in quick groups) and still enforces an average rate.
  • ✅ Uses little memory (two numbers per key) and is easy to understand. It has two settings: burst and rate.
  • ❌ Two settings to tune.

This is why token bucket is the most common choice for APIs. It is used in AWS API throttling, many API gateways, and Stripe's request limiter.

Algorithm 5: Leaky bucket

Picture a bucket with a small hole in the bottom:

  • Requests pour into the bucket (a queue is a line of waiting items).
  • They leak out and are processed at a constant rate.
  • If the bucket overflows (the queue is full), new requests are rejected.
  1. Bursty input
    ██ █ ████ █ ██████
  2. Queue
    capacity 10; overflow is rejected
  3. Steady output
    ▪ ▪ ▪ ▪ ▪ at a fixed rate
A leaky bucket turns bursty input into a steady output rate. · flow
  • ✅ A perfectly smooth output rate. It is ideal for protecting a downstream system that needs steady traffic (like a partner API or an SMS gateway).
  • ❌ Bursts are queued (which adds latency, the waiting time) or dropped. It is less friendly for interactive APIs.
  • NGINX (a popular web server) has a limit_req setting that uses the leaky bucket method.

Comparison

AlgorithmBurstsAccuracyMemory per keyBest for
Fixed windowAllows 2× at boundariesLow1 counterSimple, low-stakes limits
Sliding logNoExact1 entry per requestLow limits where precision matters (login attempts)
Sliding window counterSmoothedApproximate (good)2 countersLarge-scale limits
Token bucketAllowed up to capacityGood2 numbersMost APIs
Leaky bucketQueued/smoothedGoodQueueSteady output to weak downstream services

Distributed rate limiting

Say you have 20 API servers. Each server cannot keep its own counter. A client could get 20× the limit by spreading requests across servers. You need a shared view of the counts. This is called distributed rate limiting.

Option 1: A central store (usually Redis). Every server checks and updates the counter in Redis. A race condition can happen: two servers read "99" at the same time and both allow the request. To avoid it, make the check-and-update atomic (one step that cannot be split). Use INCR or a Lua script (a small program) that Redis runs as one step:

LUA
-- Token bucket in Redis (runs atomically)local key = KEYS[1]local capacity, rate, now = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])local b = redis.call("HMGET", key, "tokens", "ts")local tokens = tonumber(b[1]) or capacitylocal ts = tonumber(b[2]) or nowtokens = math.min(capacity, tokens + (now - ts) * rate)local allowed = 0if tokens >= 1 then tokens = tokens - 1; allowed = 1 endredis.call("HSET", key, "tokens", tokens, "ts", now)redis.call("EXPIRE", key, math.ceil(capacity / rate) * 2)return allowed
  • ✅ Accurate and simple.
  • ❌ Adds one network call per request (often about a millisecond), and Redis becomes a dependency. Decide what happens if Redis is down. Fail open means allow the traffic. This is usually right for general APIs. Fail closed means block the traffic. This may be right for login or OTP endpoints.

Option 2: Local counters with periodic sync. Each server enforces a share of the limit by itself, and shares its counts with the others every second or so. It is faster, but not exact.

Option 3: A dedicated rate-limit service, like Envoy's global rate-limit service. Gateways and proxies (servers that pass traffic on) call it.

Multi-region: counters in different regions (separate cloud areas) are hard to keep exactly the same (posts 27–28). Common ways are a separate budget for each region, or accepting that clients may go a little over the limit.

Telling clients about limits

Good APIs make limits visible to the client. They do it with HTTP headers (extra lines of information in a response):

HTTP
HTTP/1.1 429 Too Many RequestsRetry-After: 12RateLimit-Limit: 100RateLimit-Remaining: 0RateLimit-Reset: 12Content-Type: application/problem+json{"title": "Rate limit exceeded", "detail": "100 requests per minute allowed on the Free plan."}
  • 429 is the standard status code.
  • Retry-After tells the client how long to wait. Well-behaved clients (post 41) honour it.
  • RateLimit-* headers show the limit, how many requests remain, and when it resets. The IETF is still working on a standard for them as a draft, and the exact names may change. Many APIs use X-RateLimit-* variants. With these headers, clients can slow down before they hit the limit.
  • Document limits clearly per plan and endpoint.

Rate limiting vs throttling vs load shedding

  • Rate limiting: limits each client to a fair share. "You are sending too much."
  • Throttling: often means slowing requests down (making them wait in a queue) instead of rejecting them.
  • Load shedding (post 44): rejects requests because the server is overloaded, no matter who sent them. "We cannot handle more right now."

You usually need both rate limiting and load shedding. Rate limits do not protect you if all clients are within their limits but the total is still too much.

Trade-offs

  • Strict limits: strong protection and costs that you can predict, but you may block real bursts and annoy good customers.
  • Generous limits: happy users, but less protection against abuse and overload.
  • Exact algorithms (sliding log): precise, but they use a lot of memory. Approximate ones (sliding counter, token bucket): cheap and good enough for most cases.
  • Central Redis: accurate across servers, but it adds a network call and a dependency. Local counters: fast, but not exact.
  • Fail open vs fail closed when the limiter is broken: stay available, or stay protected. Choose for each endpoint.
  • IP-based limits: simple for traffic without a login, but unfair to many users behind one NAT (offices, mobile carriers). Attackers can also easily spread requests across many IPs.

In the Real World

Stripe's four limiters. Stripe wrote about using four types of limiters:

  1. a request rate limiter (a token bucket for each user),
  2. a concurrent requests limiter (how many requests a user can have running at the same time),
  3. a fleet usage load shedder (it keeps some capacity of the whole server fleet for critical requests),
  4. a worker utilisation load shedder (dropping lower-priority traffic when workers are busy).

It is a good real example of using rate limiting and load shedding together.

Cloudflare's rate limiting at scale. Cloudflare described how it built rate limiting across its global network for millions of domains. It uses a sliding-window approximation with only two counters per key, because storing the timestamp of every request would cost far too much at its scale.

GitHub's API limits. GitHub's REST API allows a set number of requests per hour for logged-in (authenticated) users, commonly 5,000, and far fewer for requests without a login (60 per hour per IP). It returns X-RateLimit-* headers with every response, so clients can pace themselves.

OTP and login protection. Banks, UPI apps (apps for instant payments in India) and most login pages limit OTP requests and login attempts per phone number, account and IP. This protects users from brute-force attacks (guessing many passwords or codes). It also protects the company from big SMS bills that bots create (this abuse is sometimes called "SMS pumping").

Interview Questions

Q1Compare fixed window, sliding window and token bucket rate limiting.

Fixed window counts per calendar window. It is simple, but it allows up to 2× the limit around window boundaries. A sliding log is exact, but it stores every timestamp. A sliding window counter weights the previous window, so two counters give a close match to a true sliding window. A token bucket allows bursts up to its capacity and enforces an average refill rate. This is why most APIs use it.

Q2How do you rate-limit across 20 API servers?

Use a shared store like Redis with an atomic check-and-update (INCR, or a Lua script that implements the token bucket), or a dedicated rate-limit service. If not, each server counts on its own and a client gets 20× the limit. Decide whether to fail open or closed if the store is down.

Q3What should a rate-limited response look like?

Status 429 Too Many Requests with a Retry-After header. Add RateLimit (or X-RateLimit) headers that show the limit, the remaining requests and the reset time. Add a clear error body that names the plan limit. Well-behaved clients wait and pace themselves using those headers.

Q4Why isn't per-IP limiting enough?

Many real users share one IP behind NAT (offices, mobile carriers), so they get blocked unfairly. Attackers spread requests across thousands of IPs. Prefer limits per API key, user or account. Use IP limits mainly for endpoints without a login.

Q5What's the difference between rate limiting and load shedding?

Rate limiting caps each client's fair share (“you are sending too much”). Load shedding rejects work because the whole server is overloaded, no matter who sent it (“we cannot take more right now”). You need both: every client can be within its limit while the total is still more than the capacity.

Q6Leaky bucket or token bucket for calling a fragile SMS provider?

A leaky bucket. It queues bursts and releases them at a fixed rate. This is what a downstream service with a hard limit on throughput needs. A token bucket would let bursts go straight through.

Key Takeaways

  • Rate limiting caps requests per client per time window, for protection, fairness, cost control and abuse prevention.
  • Know the five algorithms: fixed window (simple, boundary bursts), sliding log (exact, memory-heavy), sliding window counter (approximate, cheap), token bucket (bursts plus average rate, the API favourite) and leaky bucket (smooth output).
  • For many servers, use a shared, atomic store (Redis + Lua) or a rate-limit service, and decide fail open vs fail closed.
  • Return 429 + Retry-After and rate-limit headers, and document limits per plan and endpoint.
  • Rate limiting protects against individual clients. Pair it with load shedding to protect against total overload.

Further Reading

  • System Design Interview by Alex Xu (chapter 4, "Design A Rate Limiter")
  • Stripe's blog post "Scaling your API with rate limiters"
  • Cloudflare's blog post on how it built rate limiting for millions of domains ("Counting things, a lot of different things")
  • Azure Architecture Center patterns "Rate Limiting" and "Throttling"
  • The NGINX blog post on rate limiting with NGINX
  • The IETF draft on standard RateLimit header fields for HTTP

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series