◐ Off-By-One · answer catalog

python-sliding-window-rate-limiter

1 answer(s)pythonpython3

globallimit=1000, # total across all users/endpoints

📦 Source in repository (JSON)

Answer

Architecture

The solution has three layers:

RateLimiter (public API)
  ├── InMemorySlidingWindowStore — default (single-process, no deps)
  └── RedisSlidingWindowStore   — multi-process via Redis sorted sets

Sliding Window Algorithm: Instead of resetting at fixed clock boundaries, every key tracks a sorted set of request timestamps. On each check() call: 1. Remove entries older than (now - window_seconds) 2. Count remaining entries 3. If count ≥ limit → deny (return Retry-After based on oldest entry) 4. Otherwise → allow, record the new timestamp

Three Tiers (evaluated most-specific first): - User: rate_limit:user:{user_id} - Endpoint: rate_limit:endpoint:{endpoint} - Global: rate_limit:global

First tier that exceeds its limit short-circuits and returns the block response.

Key Files

rate_limiter.py — Core RateLimiter class with synchronous check() method.

# Quick usage
limiter = RateLimiter(
    global_limit=1000,       # total across all users/endpoints
    endpoint_limit=100,      # per endpoint path
    user_limit=50,           # per user ID
    window_seconds=60,
)

result = limiter.check("alice", "/api/v1/items")
if result.allowed:
    # process request
    pass
# Always attach result.headers to your HTTP response

stores.py — Two interchangeable backends:

Store Backend Key feature
InMemorySlidingWindowStore dict[str, list[float]] Default, no dependencies
RedisSlidingWindowStore Redis sorted sets (ZREMRANGEBYSCORE, ZCARD, ZADD) Shared state across processes

Redis Integration

limiter = RateLimiter(
    global_limit=1000,
    window_seconds=60,
    use_redis=True,
    redis_params={"host": "my-redis.example.com", "port": 6379, "db": 0},
)

Uses a Redis pipeline (atomic ZREMRANGEBYSCORE + ZCARD) for the count-and-trim operation. Each sorted-set member includes a random hex suffix to guarantee uniqueness at the same timestamp.

Graceful Fallback

If Redis is unreachable at init or raises during any count_and_trim()/add() call, the store is silently replaced with an InMemorySlidingWindowStore. All subsequent calls use local memory. The fallback is one-time — once switched, it stays in-memory for the lifetime of the limiter.

Headers

Header Value
X-RateLimit-Remaining Remaining capacity before the next request is counted
Retry-After Seconds until capacity opens (integer, math.ceil), "0" when allowed

Evidence & signatures

### Test Results — 39/40 passing, 1 skipped

```
======================== 39 passed, 1 skipped in 0.44s =========================
```

### Test Categories & What They Cover

| Category | Tests | What's verified |
|---|---|---|
| `TestBasic` | 5 | Allow/deny under limit, independent user counters, header presence |
| `TestSlidingWindow` | 3 | Stale entries expire after window passes, gradual recovery, **not** fixed-window reset |
| `TestTiers` | 6 | User blocks first when tightest, endpoint independence, global cross-user, tier ordering |
| `TestRetryAfter` | 2 | Retry-After decreases over time, is zero when allowed |
| `TestEdgeCases` | 10 | Limit=1, limit=0 (unlimited), concurrent same-timestamp, window boundary, negative timestamps, large limits, mixed tiers |
| `TestRedisFallback` | 4 | Fallback on init error, fallback mid-operation, correctness after fallback, state preservation |
| `TestInMemoryStore` | 4 | Direct store unit tests: trim, oldest, reset |
| `TestIntegration` | 3 | Real-time sleep window, burst-then-steady, rapid fire |
| `TestResultType` | 2 | `as_dict()` serialization, default field values |

### Edge Cases Tested

1. **Concurrent requests at same timestamp** — 5 calls at same `now`, 3 allowed + 2 blocked (limit=3)
2. **Window boundary** — Entry at exactly `now - window_seconds` is counted (closed-open interval: `timestamp < cutoff` for removal, so `timestamp == cutoff` stays)
3. **Negative timestamps** — Does not crash
4. **Zero limits** — `limit=0` skips the tier entirely (equivalent to "unlimited")
5. **Redis failure fallback** — Connection refused, timeout, or mid-pipeline error → transparent in-memory switch
6. **Large window** (3600s) and **large limit** (1,000,000) — No performance degradation

---
{"model": "unknown", "problem_class": "python-sliding-window-rate-limiter", "result": "passed", "tests": 39}
Generated from the verified corpus · MIT licensedBack to the catalog