DEV Community

Tech Forge
Tech Forge

Posted on

Rate Limiting Made Simple: A Practical Guide for Developers

What Is Rate Limiting and Why Should You Care?

Rate limiting controls how many requests a client can make to your API within a specific window. Without it, a single misbehaving client (or a DDoS attack) can overwhelm your backend, degrade service for everyone, and rack up cloud bills.

In this article, I'll walk through three common rate limiting strategies with real code snippets you can use today.

Strategy 1: Fixed Window Counter

The simplest approach: track a counter per user within a fixed time period (e.g., 60 seconds).

import time
from collections import defaultdict

class FixedWindowLimiter:
    def __init__(self, max_requests=10, window_seconds=60):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = defaultdict(list)

    def allow_request(self, user_id):
        now = time.time()
        window_start = now - self.window_seconds
        # Remove old entries
        self.requests[user_id] = [t for t in self.requests[user_id] if t > window_start]
        if len(self.requests[user_id]) >= self.max_requests:
            return False
        self.requests[user_id].append(now)
        return True
Enter fullscreen mode Exit fullscreen mode

Pros: Easy to implement. Cons: Traffic spikes at window boundaries can double throughput. For example, 10 requests at second 59 and 10 more at second 60 all pass.

Strategy 2: Sliding Window Log

Instead of fixed intervals, keep a timestamp log per user and check the count over a sliding window.

import time
from collections import defaultdict

class SlidingWindowLimiter:
    def __init__(self, max_requests=10, window_seconds=60):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.logs = defaultdict(list)

    def allow_request(self, user_id):
        now = time.time()
        cutoff = now - self.window_seconds
        # Remove timestamps outside the window
        self.logs[user_id] = [t for t in self.logs[user_id] if t > cutoff]
        if len(self.logs[user_id]) >= self.max_requests:
            return False
        self.logs[user_id].append(now)
        return True
Enter fullscreen mode Exit fullscreen mode

Pros: Smooths out spikes. Cons: Memory usage grows with request volume. Fine for moderate traffic, but not for millions of requests.

Strategy 3: Token Bucket (My Favorite)

A bucket holds tokens, each request consumes one token, and tokens refill at a steady rate. Bursts are allowed up to the bucket size.

import time

class TokenBucket:
    def __init__(self, capacity=10, refill_rate=1):
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second
        self.tokens = capacity
        self.last_refill = time.time()

    def allow_request(self):
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False
Enter fullscreen mode Exit fullscreen mode

Pros: Handles bursts well, memory efficient. Cons: Slightly more complex logic.

Choosing the Right Strategy

  • Fixed window is fine for low-traffic internal tools.
  • Sliding window works for most public APIs with moderate traffic.
  • Token bucket scales best for high-throughput systems (like payment APIs or real-time services).

Common Pitfalls

  • Don't forget to clean up stale data. In-memory maps grow unbounded if you don't purge old entries.
  • Use distributed storage (Redis, Memcached) when you have multiple server instances. Otherwise, each server has its own counter, and a client can send more requests than intended.
  • Always return clear HTTP headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Clients need to know when they can retry.

Quick Redis Example (Token Bucket)

import redis
import time

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

def allow_request_redis(user_id, capacity=10, refill_rate=1):
    key = f"bucket:{user_id}"
    now = time.time()
    # Lua script for atomicity (simplified here)
    tokens = r.get(key)
    if tokens is None:
        r.setex(key, 60, capacity - 1)  # TTL 60s
        return True
    tokens = float(tokens)
    if tokens >= 1:
        r.decrby(key, 1)
        return True
    return False
Enter fullscreen mode Exit fullscreen mode

For production, use a Lua script to atomically get, refill, and consume tokens.

Final Thoughts

Rate limiting is not optional. Pick a strategy that matches your traffic patterns, store state centrally if you scale horizontally, and always inform clients about their limits. Start with the fixed window if you're prototyping, then move to token bucket when you need to handle bursts gracefully.

Happy coding!

Top comments (0)