Distributed rate limits with Redis

The in-memory limiter resets when the process restarts and gives every replica its own counter. Behind a load balancer that means a user with N replicas behind them gets N times the budget. The fix is to store timestamps in Redis instead.

rate_limiter.py
python
import time
import redis

class RedisRateLimiter:
    def __init__(self, client: redis.Redis, window_seconds: int = 60, max_requests: int = 10) -> None:
        self.client = client
        self.window = window_seconds
        self.max_requests = max_requests

    def check_rate_limit(self, username: str) -> bool:
        key = f"rl:{username}"
        now = time.time()
        cutoff = now - self.window

        pipe = self.client.pipeline()
        pipe.zremrangebyscore(key, 0, cutoff)
        pipe.zcard(key)
        pipe.zadd(key, {str(now): now})
        pipe.expire(key, self.window)
        _, count, _, _ = pipe.execute()

        if count >= self.max_requests:
            self.client.zrem(key, str(now))
            return False
        return True

A Redis sorted set keyed by username holds the timestamps. The pipeline drops expired members, counts what is left, and adds the new entry in one round trip.

A sorted set is the right shape because Redis can drop expired members in a single ZREMRANGEBYSCORE call. The cost stays constant even as a user accumulates history. Same algorithm as the in-memory version, persistent state.

Quiz: Quiz

Loading practice…