From e653a9f1fdd6fbe84af2bda38ecf0014ba63b441 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 12 Apr 2026 18:05:18 -0400 Subject: [PATCH] Fix race condition in rate limiter Cleanup, length check, and append were separate non-atomic steps. Under concurrent requests a client could exceed the limit. Added a threading lock around the critical section. Co-Authored-By: Claude Opus 4.6 (1M context) --- responder/ext/ratelimit.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/responder/ext/ratelimit.py b/responder/ext/ratelimit.py index c846ea1..5cc868c 100644 --- a/responder/ext/ratelimit.py +++ b/responder/ext/ratelimit.py @@ -1,5 +1,6 @@ """Simple in-memory rate limiter for Responder.""" +import threading import time from collections import defaultdict @@ -28,6 +29,7 @@ class RateLimiter: self.max_requests = requests self.period = period self._buckets: dict[str, list[float]] = defaultdict(list) + self._lock = threading.Lock() def _client_key(self, req): client = req.client @@ -45,16 +47,19 @@ class RateLimiter: def check(self, req, resp): """Check rate limit. Sets 429 status if exceeded.""" key = self._client_key(req) - self._cleanup(key) - if len(self._buckets[key]) >= self.max_requests: - resp.status_code = 429 - resp.media = {"error": "rate limit exceeded"} - resp.headers["Retry-After"] = str(self.period) - return False + with self._lock: + self._cleanup(key) + + if len(self._buckets[key]) >= self.max_requests: + resp.status_code = 429 + resp.media = {"error": "rate limit exceeded"} + resp.headers["Retry-After"] = str(self.period) + return False + + self._buckets[key].append(time.time()) + remaining = self.max_requests - len(self._buckets[key]) - self._buckets[key].append(time.time()) - remaining = self.max_requests - len(self._buckets[key]) resp.headers["X-RateLimit-Limit"] = str(self.max_requests) resp.headers["X-RateLimit-Remaining"] = str(remaining) return True