Learn / DS & Algo / System Design / Rate Limiter

System Design · Lesson 7 of 15

Rate Limiter

Token bucket, sliding window, Redis counters and where to place a limiter.

  • Advanced
  • 38 min read
  • 3 objectives

Before this lessonLesson 6: URL Shortener

What you will learn

  • Pick an algorithm for burst vs average
  • Place the limiter in the stack
  • Scale counters in Redis

Your Progress

0 of 15 lessons 0%

  • Lessons0 / 15
  • Completed0
  • Est. time left~ 8 hours

Create a free account to keep your progress on every device.

A rate limiter caps how often a client may call an API. You will add one to every public endpoint in later designs. Goals: stop abuse, protect downstreams, keep quotas fair, return HTTP 429 with a Retry-After header.

Step 1 — Clarify

  • Limit by IP, user id, API key, or all three? Different rules per route (POST /login vs GET /public)?
  • Average rate, burst, or both? Hard block vs delay?
  • Must the limit be exact across 20 app servers, or is slightly soft OK?
  • Who configures rules — engineers, or an admin API?

Assumptions: 1000 req/min per API key, bursts of 50, many app boxes, p99 of the allow/deny check under a few milliseconds, 429 + headers on deny. Login endpoints get a tighter separate rule.

Step 2 — Where it sits

flowchart TB C[Client] --> Edge[CDN / API gateway] Edge --> RL[Rate limiter] RL -->|allow| APP[App servers] RL -->|429| C RL --> Redis[(Redis cluster)]

The gateway is the usual place: one policy, before you spend app CPU. Redis holds shared counters so all boxes agree.

In-process memory on each app server is wrong: 10 boxes × 100 req/s allowed = 1000 req/s to the backend. A library inside each app that talks to Redis is acceptable if you have no gateway. CDN WAF limits are coarse (IP) and do not know your API keys — use them as a first shield, not the product limiter.

Step 3 — Algorithms

Token bucket (default)

Tokens refill at rate r per second. The bucket holds at most c tokens. Each request costs one token. Bursts up to c are allowed, then steady r. This matches how people actually use APIs.

import time

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

    def allow(self):
        now = time.monotonic()
        self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
        self.last = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

bucket = TokenBucket(rate=5, capacity=10)
print(sum(bucket.allow() for _ in range(12)))
Output
10

Leaky bucket

Queue requests and drain at a fixed rate. Smooths bursts; adds latency. Useful when a downstream cannot take spikes at all (some payment processors).

Fixed window counter

Count hits in the current minute; reset on the minute boundary. Cheap (INCR + EXPIRE). Edge burst: a client can fire limit at 12:00:59 and again at 12:01:00 — twice the intended rate. Mention this flaw.

Sliding window log

Store a timestamp per request, drop those older than the window. Accurate, memory-heavy at high QPS.

Sliding window counter

Weight the previous window by how much of it still overlaps. Good Redis compromise: two integers, not a log.

def allow(key, limit, now_s, counts):
    window = now_s // 60
    prev, cur = counts.get(window - 1, 0), counts.get(window, 0)
    frac = (now_s % 60) / 60
    approx = cur + prev * (1 - frac)
    return approx < limit, round(approx, 1)

print(allow("user:42", 100, 60 + 15, {1: 40, 2: 30}))
Output
(True, 60.0)

Step 4 — Distributed implementation

Use Redis. Shard by client key (consistent hash) so one hot API key hits one Redis slot, not a broadcast. A Lua script makes refill + consume atomic for a token bucket. For a fixed window, INCR then EXPIRE on first increment.

  • Cache the last allow/deny in the gateway for 10–50 ms so a hot key does not hit Redis on every request.
  • Fail open vs fail closed: if Redis is down, a public read API might fail open (serve traffic, log); a login or SMS send should fail closed (429).
  • Return 429, Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining.

Step 5 — Rules you should name

  • Global API quota per key, plus per-route overrides (login, password reset, create-short-url).
  • Paying customers get a larger bucket, not a different algorithm.
  • Store rules in a config service, cache them; changing a limit should not require a deploy.
// Write your solution here
Up next · Lesson 8Key-Value Store and PastebinA Dynamo-style KV store, then Pastebin as a thin API on top of it.