system-design · intermediate

Rate Limiting Algorithms — Token Bucket, Windows, and Bursts

The Central Question

Consider an enterprise API platform running on the TrafficLab platform (trafficlab.com) processing 50,000 requests per second across global microservices:


Without traffic governance, a single un-throttled client can crash a multi-tenant platform.

Rate limiting is the operational control mechanism that caps how many requests a client may execute within a designated time window, protecting backend stability and maintaining fair resource allocation across tenants.

This lesson answers one central question: How do rate limiting algorithms (Token Bucket, Leaky Bucket, Fixed Window Counter, Sliding Window Counter) govern API traffic volume, mathematically manage bursts, and enforce fairness using distributed Redis storage patterns?


The Four Core Rate Limiting Algorithms

Engineers implement rate limits using four primary algorithms, each offering different trade-offs between memory efficiency, burst tolerance, and traffic smoothness:

flowchart TD
  Alg[Rate Limiting Algorithms] --> FB[1. Fixed Window Counter]
  Alg --> SW[2. Sliding Window Counter]
  Alg --> TB[3. Token Bucket]
  Alg --> LB[4. Leaky Bucket]
  
  FB --> FBDesc["Resets count on fixed time boundaries (e.g. 1 minute). Simple, but prone to 2x boundary spikes."]
  SW --> SWDesc["Calculates weighted average across window boundaries. Prevents boundary spikes with minimal memory."]
  TB --> TBDesc["Refills tokens continuously up to capacity. Supports short bursts while capping average rate."]
  LB --> LBDesc["Drains requests at a constant output rate using a queue. Smooths traffic bursts into a steady flow."]

Figure 1: Overview of the four primary rate limiting algorithms.


1. Fixed Window Counter Algorithm

The Fixed Window Counter algorithm divides time into static, fixed intervals (such as 1-minute windows: 12:00:00 - 12:01:00, 12:01:00 - 12:02:00). Each window maintains an isolated request counter per client.

gantt
    title Fixed Window Boundary Spike Vulnerability (Limit: 100 req/min)
    dateFormat  ss
    axisFormat %S
    section Window 1 (12:00)
    Idle Period :done, w1a, 00, 50
    50 reqs sent (12:00:50) :active, w1b, 50, 60
    section Window 2 (12:01)
    100 reqs sent (12:01:00) :crit, w2a, 60, 70
    Idle Period :done, w2b, 70, 120

Figure 2: The boundary burst vulnerability in fixed windows allowing twice the allowed rate across window edges.

Mathematical Mechanics

The Boundary Burst Vulnerability

If a client sends 100 requests at `12:00:59` (end of Window 1) and another 100 requests at `12:01:00` (start of Window 2):

2. Sliding Window Counter Algorithm

The Sliding Window Counter algorithm resolves boundary spikes without storing raw timestamp logs by combining the request count of the current window with a weighted percentage of the previous window count.

flowchart LR
  subgraph Previous Window (12:00)
    PCount["Previous Count: 80 Requests"]
  end
  subgraph Current Window (12:01 - 36 Seconds Elapsed)
    CCount["Current Count: 30 Requests"]
  end
  subgraph Sliding Window Math
    Calc["Weight = (60 - 36) / 60 = 40%<br/>Estimated Rate = (80 * 0.40) + 30 = 62 Requests"]
  end
  
  PCount --> Calc
  CCount --> Calc

Figure 3: Calculating weighted request rates in the Sliding Window Counter algorithm.

Mathematical Formula

Let $W$ be window size in seconds (e.g. $60\text{s}$). Let $t_{\text{elapsed}}$ be seconds elapsed in current window:

$$\text{Estimated Rate} = \text{Count}_{\text{current}} + \left( \text{Count}_{\text{previous}} \times \frac{W - t_{\text{elapsed}}}{W} \right)$$


3. Token Bucket Algorithm

The Token Bucket algorithm is the industry standard for public web APIs (such as Stripe and AWS). It maintains a bucket containing tokens up to a maximum Capacity ($C$), refilling tokens continuously at a fixed Refill Rate ($R$) per second.

flowchart TB
  Refill["Refill Generator: Adds R Tokens/sec"] --> Bucket["Token Bucket (Max Capacity: C)"]
  Req[Incoming HTTP Request] --> Check{Token Available?}
  Bucket -->|Check Balance| Check
  Check -->|Yes: Consume 1 Token| Allow[HTTP 200 OK: Request Allowed]
  Check -->|No: Zero Tokens| Deny[HTTP 429 Too Many Requests]

Figure 4: State machine governing token consumption and refill in the Token Bucket algorithm.

Mathematical Mechanics

Instead of running a continuous background timer to refill tokens (which wastes CPU), the token balance is calculated lazily upon request arrival using the timestamp of the last request:

$$\text{Tokens}_{\text{new}} = \min\left(C, \; \text{Tokens}_{\text{stored}} + (t_{\text{now}} - t_{\text{last}}) \times R\right)$$


4. Leaky Bucket Algorithm

The Leaky Bucket algorithm smooths traffic bursts into a steady, constant output rate using a First-In, First-Out (FIFO) queue.

flowchart TB
  BurstyInput["Bursty Traffic In: 100 reqs/sec"] --> BucketQueue["Leaky Bucket Queue (Capacity: 50)"]
  BucketQueue -->|Queue Full| Drop[HTTP 429 / Drop]
  BucketQueue -->|Drain at Fixed Rate| Outflow["Smooth Traffic Out: 10 reqs/sec"]
  Outflow --> Server[Backend Application]

Figure 5: Converting bursty input traffic into smooth outflow using the Leaky Bucket algorithm.


Complete Worked Example: Production Go Token Bucket Implementation

Let's inspect a complete, thread-safe Go implementation of the Token Bucket rate limiter for the TrafficLab platform (trafficlab.com).

package main

import (
"fmt"
"math"
"sync"
"time"
)

type TokenBucket struct {
mu sync.Mutex
capacity float64
tokens float64
refillRate float64 // Tokens per second
lastRefilled time.Time
}

func NewTokenBucket(capacity float64, refillRate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity,
refillRate: refillRate,
lastRefilled: time.Now(),
}
}

func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()

now := time.Now()
elapsed := now.Sub(tb.lastRefilled).Seconds()
tb.lastRefilled = now

// 1. Refill tokens lazily
tb.tokens = math.Min(tb.capacity, tb.tokens+elapsed*tb.refillRate)

// 2. Consume token if available
if tb.tokens >= 1.0 {
tb.tokens -= 1.0
return true
}

return false
}

func main() {
limiter := NewTokenBucket(5, 1) // Capacity: 5, Refill: 1 token/sec

// Simulate 10 rapid request bursts
for i := 1; i <= 10; i++ {
if limiter.Allow() {
fmt.Printf("Request %d: ALLOWED (HTTP 200)\n", i)
} else {
fmt.Printf("Request %d: DENIED (HTTP 429 Too Many Requests)\n", i)
}
time.Sleep(100 * time.Millisecond)
}
}


Failure Modes and Engineering

Rate Limit Tier Matrix

Client TierTarget EndpointsAlgorithm UsedQuota / Refill RateBurst CapacityRejection Policy
Anonymous IPGET /v1/catalog/*Sliding Window Counter30 req / minute0 (Strict window)HTTP 429 + Retry-After: 60
Standard UserAll Authenticated APIsToken Bucket10 tokens / second30 tokensHTTP 429 + Retry-After: 1
Enterprise PartnerPOST /v1/ordersToken Bucket100 tokens / second300 tokensHTTP 429 + Retry-After: 1
Login EndpointPOST /v1/auth/loginFixed Window (IP + User)5 req / 15 minutes0 (Brute force protection)HTTP 429 + Retry-After: 900

Hierarchical Rate Limiting & Multi-Layer Quotas

Enterprise systems enforce rate limits across multiple architectural layers simultaneously (**Hierarchical Rate Limiting**). A single incoming API request must satisfy three independent checks:
  1. Global Gateway Limit (e.g. Max 50,000 RPS across the entire platform to protect edge load balancers).
  2. Tenant Account Limit (e.g. Max 1,000 RPS per customer account to enforce billing contract tiers).
  3. Endpoint-Specific Limit (e.g. Max 5 RPS on POST /v1/reports/export to prevent CPU-intensive database queries).
If any layer in the hierarchy rejects the request, the API Gateway short-circuits execution immediately, returning HTTP 429 without invoking downstream microservices.

Dynamic Rate Limits During Incidents

Static rate limits cannot adapt when a backend database is degraded. Modern API Gateways integrate with internal observability metrics (such as database connection pool latency or server CPU utilization) to enforce **Dynamic Rate Limiting**. When primary database CPU exceeds 85%, the gateway automatically reduces free-tier user quotas by 50% dynamically until CPU utilization stabilizes, preserving operational capacity for paid enterprise customers.

Rate Limiting Key Selection Best Practices

Designing an effective rate limit key requires carefully identifying the client identity boundary:
Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Distributed Redis Rate Limit OutageCentralized Redis cluster memory exhausts or experiences network partition.API Gateway fails all client requests with HTTP 500 errors.Redis connection failure alerts and HTTP 5xx error spikes.Implement explicit Fail-Open policies for non-critical reads, allowing traffic through while emitting operational warnings.
2. Corporate NAT IP ThrottlingRate limiting authenticated traffic strictly by client source IP address.500 employees behind a corporate NAT firewall get blocked when 1 employee makes heavy API calls.Customer complaint spikes from corporate IP ranges.Rate limit authenticated traffic by User ID or API Key; restrict IP-based rate limiting to un-authenticated routes.
3. Client Retry Storm on 429Clients receive HTTP 429 and immediately retry in a tight while(true) loop.Traffic volume multiplies by 10x during an overload event, preventing recovery.Incoming request RPS spikes following 429 response bursts.Include Retry-After headers and mandate client exponential backoff with full jitter in client SDKs.
4. Un-protected Internal MicroservicesRate limits enforced only at public edge; internal service calls have zero quotas.An internal background worker bug floods internal payment microservices with 50,000 req/sec.Internal microservice CPU saturation and cascading failures.Implement internal rate limiting and backpressure controls on east-west microservice traffic.

What You Should Remember

  1. Rate limiting protects stability and fairness: Rate limits prevent a single buggy or malicious client from consuming all system capacity.
  2. Token Bucket supports controlled bursts: Token Bucket ($C, R$) allows short request spikes up to capacity $C$ while enforcing long-term average rate $R$.
  3. Leaky Bucket forces constant outflow: Leaky Bucket queues requests to output a smooth, constant execution rate, ideal for traffic shaping.
  4. Use Redis Lua scripts for distributed enforcement: Atomic Lua script execution in Redis prevents concurrency race conditions across multi-node gateway pools.
  5. Return standard 429 headers: Always return HTTP 429 Too Many Requests with Retry-After headers and RFC 7807 error details.

Glossary of Terms

TermDefinition
Rate LimitingThe mechanism that caps the maximum number of requests a client can execute within a specific time window.
Token BucketAn algorithm where tokens refill at rate $R$ up to capacity $C$, allowing requests to spend tokens.
Leaky BucketAn algorithm that queues requests to drain them at a constant, smooth output rate.
Fixed Window CounterAn algorithm that counts requests within fixed time intervals, vulnerable to 2x boundary spikes.
Sliding Window CounterAn algorithm that estimates request rates using a weighted combination of current and previous window counts.
Burst CapacityThe maximum number of requests a client can execute instantaneously above its long-term average rate.
Redis Lua ScriptAn atomic script executed on a Redis server to perform thread-safe rate limit evaluations.
Fail-OpenAn operational policy that permits traffic through when rate limiting infrastructure fails.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing rate limits for a financial trading API (`trade.trafficlab.com`):
  1. GET /v1/market/prices (High volume, lightweight read)
  2. POST /v1/trading/orders (Critical mutation, sensitive to database lock contention)
  3. POST /v1/auth/login (Authentication endpoint, target for brute-force attacks)
**Questions**:
  1. Recommend the optimal rate limiting algorithm (Token Bucket vs Sliding Window vs Fixed Window) and policy configuration for each endpoint.
  2. Formulate how your Token Bucket implementation calculates token refills lazily without running background timer threads.

Interactive Self-Assessment

It supports controlled traffic bursts up to capacity C while preventing 2x boundary spikes.

It consumes zero memory in Redis databases.

It forces incoming requests to execute at a strictly constant output rate.

It eliminates the need for HTTP 429 status codes.

It guarantees atomic, thread-safe execution, preventing multi-instance concurrency race conditions.

It allows client web browsers to execute rate limiting math locally.

It bypasses the need for in-memory key storage in Redis.

It converts HTTP status codes to TCP transport packets.


What to Learn Next

Track: Distributed Systems

Next: Design a URL Shortener — Interview Walkthrough

Series: Rate Limiting

  1. Rate Limiting Algorithms — Token Bucket, Windows, and Bursts (this guide)
  2. Rate Limiter Design — Case Study
  3. Distributed Rate Limiting — Shared Quotas Across Many Pods
  4. Circuit Breakers & Rate Limiting Together

All series

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab