System Design

System Design Adventure - 17

Rate Limiting: Protecting Your System From Too Much of a Good Thing

Posted by Afsal on 23 Oct 2026
Hi Pythonistas!
 
Your API is live.
Users are using it.
Everything fine.
 
Then one day:
Normal traffic:  100 requests/second
Suddenly:        50,000 requests/second
 
Could be:
  • a popular blog post linked your API.
  • a mobile app bug causing infinite retries.
  • a bot scraping your data.
  • a DDoS attack.
Your server handles 1,000 requests/second.
50,000 hits it.
Server crashes.
Everyone affected.
Including legitimate users.
Rate limiting prevents this.
 
What Is Rate Limiting?
 
Controlling how many requests
 
a client can make in a given time window.
Rule: max 100 requests per minute per user
Request 1-100  → allowed ✅
Request 101    → rejected ❌ HTTP 429 Too Many Requests
Protects your server.
Fair usage for all users.
Prevents abuse.
 
Where to Apply
 
IP level:
 
Max 1000 requests/minute per IP
Protects against bots.
But many users share one IP (offices, NAT).
Unfair to block entire office.
 
User level:
 
Max 100 requests/minute per user_id
More precise.
Authenticated users only.
 
API key level:
 
Free tier:        100 requests/day
Pro tier:         10,000 requests/day
Enterprise tier:  unlimited
How Stripe, OpenAI, Twilio charge for usage.
 
Endpoint level:

POST /login:   5 requests/minute   (prevent brute force)
GET /feed:     60 requests/minute  (normal usage)
POST /upload:  10 requests/minute  (expensive operation)
Different limits for different endpoints.
Expensive operations get stricter limits.
 
Algorithm 1 - Fixed Window Counter
 
Simplest.
Divide time into fixed windows.
Count requests in each window.
Window: 1 minute
Limit: 100 requests
 
12:00:00 - 12:01:00 → window 1
12:01:00 - 12:02:00 → window 2
 
Good: simple, memory efficient.
Bad: boundary attack.
99 requests at 12:00:59 (end of window 1)
99 requests at 12:01:01 (start of window 2)
Total: 198 requests in 2 seconds.
Both windows show < 100.
Limit bypassed.
 
Algorithm 2 - Sliding Window Log
 
Track timestamp of every request.
Count requests in last N seconds.
 
Good: accurate, no boundary attack.
Bad: high memory.
Store every request timestamp.
1 million users × 100 requests = 100 million timestamps.
 
Algorithm 3 - Sliding Window Counter
 
Hybrid of both.
Two windows. Weighted count.
 
Good: accurate, memory efficient, no boundary attack.
Bad: approximate (tiny margin of error).
Used by Cloudflare and many large systems.
 
Algorithm 4 - Token Bucket
 
Most popular.
Imagine a bucket of tokens.
Bucket capacity: 100 tokens
Refill rate:     10 tokens/second
Each request consumes 1 token.
Bucket refills continuously.
Start:          100 tokens
10 requests  →  90 tokens
Refill +10   →  100 tokens
100 requests →  0 tokens
Next request →  rejected (no tokens)
 
Good: allows bursts, smooth refill, natural feeling.
Bad: needs syncing across distributed servers.
 
Algorithm 5 - Leaky Bucket
 
Requests come in at any rate.
Processed at fixed rate.
Excess rejected.
Requests → [bucket/queue] → processed at fixed rate
overflow → rejected
Like a leaky bucket:
water pours in at any rate.
leaks out constantly.
overflow spills.
Good: smooth output, prevents bursts hitting server.
Bad: burst requests rejected, not good for bursty traffic.
 
Algorithms Compared
Algorithm              Accuracy  Memory  Burst  Complexity
───────────────────────────────────────────────────────────
Fixed Window           Low       Low     Yes    Simple
Sliding Window Log     High      High    No     Medium
Sliding Window Counter High      Low     No     Medium
Token Bucket           High      Low     Yes    Medium
Leaky Bucket           High      Medium  No     Medium
 
Simple systems    → Fixed Window
Accuracy needed   → Sliding Window Counter
Burst traffic ok  → Token Bucket
Smooth output     → Leaky Bucket
 
Distributed Rate Limiting
 
Multiple servers.
Each tracking count independently:
User limit: 100 requests/minute
 
Server1: 50 requests
Server2: 50 requests
Server3: 50 requests
Total:   150 requests → limit bypassed
Solution: Redis as shared counter.
 
All servers increment same Redis key.
Accurate. Atomic. No race conditions.
 
Rate Limit Headers
 
Tell clients about their limits.
X-RateLimit-Limit:     100
X-RateLimit-Remaining: 45
X-RateLimit-Reset:     1705312800
Retry-After:           30
 
Good clients use these headers.
Back off when remaining is low.
Retry after Reset time.
 
Implementation in Python Using slowapi
from fastapi import FastAPI
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app = FastAPI()

@app.get('/api/feed')
@limiter.limit('60/minute')
async def get_feed(request: Request):
    return {'feed': [...]}

@app.post('/api/login')
@limiter.limit('5/minute')
async def login(request: Request):
    return {'token': '...'}
 
Using Redis directly:
 
def rate_limit(user_id, limit=100, window=60):
    now = int(time.time())
    window_key = now // window
    key = f'rate:{user_id}:{window_key}'


    pipe = r.pipeline()
    pipe.incr(key)
    pipe.expire(key, window)
    results = pipe.execute()
    count = results[0]


    return {
        'allowed': count <= limit,
        'remaining': max(0, limit - count),
        'reset': (window_key + 1) * window
    }


def api_handler(request):
    result = rate_limit(request.user_id)


    if not result['allowed']:
        return {
            'error': 'Too many requests',
            'retry_after': result['reset'] - time.time()
        }, 429


    return handle_request(request)
 
Rate Limiting at Different Layers
 
Nginx
http {
    limit_req_zone $binary_remote_addr
        zone=api:10m rate=100r/m;


    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
        }
    }
}
 
Blocks before request reaches your app.
Fast. Efficient.
 
API Gateway
plugins:
  - name: rate-limiting
    config:
      minute: 100
      hour: 1000
      policy: redis
      redis_host: redis.internal
 
Managed. No code changes.
 
Cloudflare:
 
Rate limiting at CDN level.
Before request reaches your servers.
Best protection against DDoS.
 
Handling Rate Limited Responses
 
Exponential backoff
def api_call_with_retry(url, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url)


        if response.status_code == 429:
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f'Rate limited. Waiting {wait:.2f}s')
            time.sleep(wait)
            continue


        return response
    raise Exception('Max retries exceeded')
 
Wait 1s, 2s, 4s, 8s, 16s.
 
Random jitter prevents stampede.(Remember Post 8A?)
 
Respect Retry-After header
if response.status_code == 429:
    retry_after = int(response.headers.get('Retry-After', 60))
    time.sleep(retry_after)
Don't guess. Use the header.
 
Rate Limiting vs Throttling
 
Often confused.
 
Rate Limiting  → hard stop
               → request rejected
               → HTTP 429
 
Throttling     → slow down
               → request delayed
               → eventually processed
 
Use rate limiting for:
  • API protection
  • Fair usage enforcement
  • Preventing abuse
Use throttling for:
  • Internal services
  • Batch processing
  • Where slow is better than rejected
Mental Model
 
Rate limiting          → control requests per time window
Fixed window           → simple, boundary attack possible
Sliding window log     → accurate, high memory
Sliding window counter → accurate, memory efficient
Token bucket           → allows bursts, smooth refill
Leaky bucket           → constant output, no bursts
Redis counter          → distributed rate limiting
429                    → HTTP status for rate limited
X-RateLimit-*          → headers to inform clients
Retry-After            → when client can retry
Exponential backoff    → client retry strategy
Rate limiting          → hard reject
Throttling             → slow down, eventually process
 
What's Coming Next
 
Now your system handles too much traffic.
But what about when your own services fail?
Service A calls Service B.
Service B is slow.
Service A waits.
Threads pile up.
Service A crashes.
Now both are down.
Here comes Circuit Breakers How to stop one failure from taking down everything.
← Previous Post

Recent posts