globallimit=1000, # total across all users/endpoints
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.
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 |
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.
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.
| 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 |
### 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}