"""In-process sliding-window rate limiter. IMPORTANT — deployment note: Rate limit counters are stored in memory and are lost on process restart. The bucket key is the client address from grimoire/proxy.py, read by TRUSTED_PROXY_HOPS. A hop count that is too low keys every caller on the proxy's address, so they all share one bucket. """ import asyncio import time from collections import defaultdict _buckets: dict[str, list[float]] = defaultdict(list) _lock = asyncio.Lock() async def is_rate_limited(key: str, max_requests: int, window_seconds: int) -> bool: """Returns True if request should be blocked (limit exceeded).""" async with _lock: now = time.monotonic() cutoff = now - window_seconds _buckets[key] = [t for t in _buckets[key] if t > cutoff] if len(_buckets[key]) >= max_requests: return True _buckets[key].append(now) return False