What Is a Rate Limiter?

A rate limiter caps how many requests a user, IP address or API key can make in a period of time, for example 100 requests per minute. Requests over the limit are rejected, usually with HTTP 429 Too Many Requests.

Every serious API has one. It's what stops one buggy script, scraper or attacker from using up capacity meant for everyone. It's also one of the most common system design interview questions.


🎯 Quick Answer (30-Second Read)

  • Purpose: protect services from overload and abuse, and keep usage fair between users.
  • Response: over the limit → 429 Too Many Requests, often with a Retry-After header.
  • Main algorithms: token bucket (allows bursts), leaky bucket (smooth output), fixed window (simple), sliding window (accurate).
  • Most popular: token bucket, used widely by API gateways and cloud providers.
  • Distributed systems keep counters in a shared store like Redis so every server sees the same count.

Why Rate Limiting Matters

  • Stops overload: a traffic spike or retry storm can't take the whole system down.
  • Blocks abuse: brute-force logins, scraping and spam get cut off.
  • Fairness: one heavy user can't starve everyone else.
  • Cost control: caps usage of expensive downstream services, such as paid AI APIs.
Key takeaway: a 429 error is a feature. It's far better to reject 5% of requests quickly than to let 100% of requests time out because the system collapsed.

The Four Main Algorithms

1. Token bucket

A bucket holds up to N tokens and refills at a steady rate. Each request takes one token. No tokens left means the request is rejected.

  • ✅ Allows short bursts (up to the bucket size) while enforcing an average rate
  • ✅ Memory-cheap: two numbers per user (tokens left, last refill time)
  • Used widely by API gateways and cloud APIs
stateDiagram-v2 [*] --> HasTokens HasTokens --> HasTokens: request → take 1 token (allowed) HasTokens --> Empty: last token used Empty --> Empty: request → 429 rejected Empty --> HasTokens: refill over time

2. Leaky bucket

Requests enter a queue (the bucket) and are processed at a constant rate, like water leaking from a hole. If the queue is full, new requests are dropped.

  • ✅ Very smooth output, which protects fragile downstream systems
  • ❌ Doesn't allow bursts, and queued requests wait

3. Fixed window counter

Count requests per fixed window (for example per calendar minute) and reset at the boundary.

  • ✅ Simplest to build
  • ❌ Boundary problem: a user can send 100 requests at 12:00:59 and another 100 at 12:01:00, so 200 requests land in two seconds.

4. Sliding window

Fixes the boundary problem by looking at the last 60 seconds at any moment.

  • Sliding window log: stores a timestamp per request. Exact, but memory-heavy.
  • Sliding window counter: blends the current and previous window counts. A good approximation that's cheap.

Which Algorithm Should You Use?

Algorithm Bursts Accuracy Memory Best for
Token bucket ✅ Allowed Good Very low Public APIs, general use
Leaky bucket ❌ Smoothed Good Low–medium Protecting slow downstream systems
Fixed window ⚠️ At edges Rough Very low Simple internal limits
Sliding window log ✅ Exact Exact High Low-volume, strict limits
Sliding window counter Mostly Very good Low High-volume APIs

Step-by-Step: A Token Bucket in Python

A minimal, single-server version to understand the idea:

import time

class TokenBucket:
    def __init__(self, capacity: int, refill_per_sec: float):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_per_sec = refill_per_sec
        self.last = time.monotonic()

    def allow(self) -> bool:
        now = time.monotonic()
        # add tokens for the time that passed, up to capacity
        self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.refill_per_sec)
        self.last = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

bucket = TokenBucket(capacity=10, refill_per_sec=1)   # bursts of 10, 1 req/s average
print([bucket.allow() for _ in range(12)])            # first 10 True, then False

In production, you'd keep these counters in Redis so all servers share them, and update them atomically (with a Lua script or atomic commands) so two servers can't both spend the last token.


Where Rate Limiters Live

  • API gateway / load balancer (most common): limits before traffic reaches your app
  • CDN / edge (Cloudflare, AWS WAF): blocks abusive traffic close to the user
  • Inside the app: per-feature limits such as "5 password resets per hour"
  • Client side: good clients respect Retry-After and back off with jitter

Distributed Rate Limiting Architecture

With one server, a counter in memory works. With ten servers behind a load balancer, each server only sees a tenth of a user's requests, so in-memory limits stop working. The usual fix is a shared counter store, typically Redis:

flowchart LR C["Clients"] --> LB["Load balancer"] LB --> G1["Gateway 1
rate limiter"] LB --> G2["Gateway 2
rate limiter"] G1 <-->|"INCR user:42 + expiry
(atomic)"| R[("Redis
shared counters")] G2 <--> R G1 -->|allowed| API["API servers"] G2 -->|allowed| API G1 -.->|over limit| X["429 Too Many Requests"] CFG[("Rules config
free: 60/min · pro: 600/min")] -.-> G1 CFG -.-> G2

Design decisions to make:

  • Atomic updates: check-and-increment must happen in one step (a Redis INCR, or a Lua script for token buckets), or two servers can both let request number 61 through.
  • Fail open or fail closed: if Redis is down, do you allow all traffic (keep the product working) or block it (protect the backend)? Most public APIs fail open.
  • Latency: the counter check adds a network hop on every request, so keep Redis close to the gateways.
  • Approximate is fine: a few extra requests slipping through at a window edge rarely matters. Exact global counting across regions is expensive.

Good 429 Responses

A helpful rate-limited response tells clients what to do:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{"error": "rate_limited", "message": "Too many requests. Try again in 30 seconds."}

Clients should wait for the Retry-After time and use exponential backoff with jitter. Retrying instantly makes overload worse. See Why Apps Crash During High Traffic.


Frequently Asked Questions

What is the difference between rate limiting and throttling?

The terms are often used interchangeably. Strictly, rate limiting rejects requests over a limit (with a 429 error), while throttling slows them down, for example by queueing or delaying them as a leaky bucket does. Many systems combine both.

How do you rate limit across many servers?

Store the counters in a shared, fast store such as Redis, keyed by user or API key, and update them atomically so concurrent requests can't both pass. Alternatives include sticky routing (so one server sees all of a user's traffic) or approximate per-server limits that add up to the global one.

Should I rate limit by IP address or by user?

Both, for different reasons. Per-IP limits protect unauthenticated endpoints such as login and signup. Per-user or per-API-key limits are fairer for logged-in traffic, because many users can share one IP address behind offices, colleges or mobile networks.


Conclusion

A rate limiter protects your system by capping requests per user, IP or key and returning 429 when they go over. Token bucket is the go-to choice because it allows bursts while enforcing an average rate. Use sliding windows when you need accuracy, and keep counters in Redis once you run more than one server.


Sources

Last verified: 14 October 2026.


Related reads: Load Balancer Explained · Why Apps Crash During High Traffic · Redis Explained · Microservices Architecture Explained

I write about system design and how real products are built. Follow on LinkedIn or X, or invite me to speak.