diff --git a/backend/app/rate_limit.py b/backend/app/rate_limit.py new file mode 100644 index 0000000..d6dc4c6 --- /dev/null +++ b/backend/app/rate_limit.py @@ -0,0 +1,22 @@ +import time +from collections import defaultdict + + +class RateLimiter: + """Fixed-window limiter keyed by an arbitrary string (user id or client IP).""" + + def __init__(self, max_requests: int, window_seconds: float): + self.max_requests = max_requests + self.window_seconds = window_seconds + self._hits: dict[str, list[float]] = defaultdict(list) + + def allow(self, key: str) -> bool: + now = time.monotonic() + window_start = now - self.window_seconds + hits = self._hits[key] + while hits and hits[0] < window_start: + hits.pop(0) + if len(hits) >= self.max_requests: + return False + hits.append(now) + return True diff --git a/backend/tests/test_rate_limit.py b/backend/tests/test_rate_limit.py new file mode 100644 index 0000000..78790d1 --- /dev/null +++ b/backend/tests/test_rate_limit.py @@ -0,0 +1,16 @@ +from app.rate_limit import RateLimiter + + +def test_allows_up_to_limit_then_blocks(): + limiter = RateLimiter(max_requests=3, window_seconds=60) + assert limiter.allow("user-1") is True + assert limiter.allow("user-1") is True + assert limiter.allow("user-1") is True + assert limiter.allow("user-1") is False + + +def test_different_keys_tracked_independently(): + limiter = RateLimiter(max_requests=1, window_seconds=60) + assert limiter.allow("user-1") is True + assert limiter.allow("user-2") is True + assert limiter.allow("user-1") is False