python 118 lines · 3 tabs

Sliding-Window Per-User Rate Limiting With Redis and a Flask Decorator

Shared by codesnips Sep 2026
3 tabs
import time
import uuid
from dataclasses import dataclass

import redis

_LUA = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
    redis.call('ZADD', key, now, member)
    redis.call('EXPIRE', key, window)
    return {1, limit - count - 1}
end
redis.call('EXPIRE', key, window)
return {0, 0}
"""


@dataclass
class RateLimitResult:
    allowed: bool
    remaining: int
    retry_after: int


class SlidingWindowLimiter:
    def __init__(self, client, limit, window):
        self.client = client
        self.limit = limit
        self.window = window
        self._script = client.register_script(_LUA)

    def is_allowed(self, key):
        now = time.time()
        member = "{0}-{1}".format(now, uuid.uuid4().hex)
        allowed, remaining = self._script(
            keys=[key],
            args=[now, self.window, self.limit, member],
        )
        retry_after = 0 if allowed else self.window
        return RateLimitResult(
            allowed=bool(allowed),
            remaining=int(remaining),
            retry_after=int(retry_after),
        )
3 files · python Explain with highlit

This snippet implements a per-user rate limiter using a sliding-window counter backed by Redis. A sliding window fixes the classic problem of fixed-window counters, where a burst at the end of one window plus a burst at the start of the next can let through nearly double the intended limit. Instead, the algorithm keeps a sorted set of request timestamps per key and counts only those falling inside the trailing window seconds, so the boundary moves continuously with time.

The SlidingWindowLimiter class in rate_limiter.py wraps the whole operation in a single Redis Lua script. Doing the read-modify-write inside Lua matters: ZREMRANGEBYSCORE, ZCARD, and the conditional ZADD all run atomically on the server, so two concurrent requests can never both read a stale count and both slip past the limit. The script trims entries older than now - window, counts what remains, and only records the new request when the count is under limit; it returns both the allow/deny decision and the number of remaining slots. EXPIRE keeps idle keys from leaking memory.

The is_allowed method loads the script once via register_script and returns a small RateLimitResult dataclass carrying allowed, remaining, and a computed retry_after. Time is passed in from Python rather than read inside Lua so the logic stays testable and deterministic.

In decorator.py, rate_limit adapts the limiter to Flask. It resolves a per-user identity through a pluggable key_func (defaulting to the authenticated user id, falling back to the client IP), builds a namespaced Redis key, and aborts with 429 when the window is full. It always sets X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers so clients can back off intelligently, using Flask's after_this_request to attach them to the eventual response.

app.py wires it together, sharing one limiter instance and applying different budgets per route. The trade-offs worth noting: the sorted set stores one member per request, so very high-traffic keys cost more memory than a bare counter, and the limiter depends on Redis availability — a fail-open or fail-closed policy on connection errors is a deliberate decision the caller should make.


Related snips

Share this code

Here's the card — post it anywhere.

Sliding-Window Per-User Rate Limiting With Redis and a Flask Decorator — share card
Link copied