Implement a rule-based rate limiter
CloudflareSoftware Engineer · OnsiteLast updated
Design and implement a multi-rule rate limiter that tracks client request quotas across matching path prefixes using a sliding time window.
Problem Statement
In reverse proxies and edge networks (such as Cloudflare), incoming HTTP requests are evaluated against a set of firewall and rate-limiting rules. Implement an in-memory, rule-based rate limiter that enforces sliding window rate limits across configurable URL path prefixes.
Implement the RateLimiter class with the following methods:
-
add_rule(rule_id: str, path_prefix: str, limit: int, window_seconds: int) -> None: Registers a new rate-limiting rule. Any request whose path starts withpath_prefixis subject to this rule. A client is permitted at mostlimitrequests within any rolling interval ofwindow_seconds. -
allow_request(timestamp: int, client_id: str, path: str) -> bool: Evaluates an incoming request fromclient_idfor URLpathat timetimestamp(in seconds).- A request matches a rule if
path.startswith(path_prefix). - A request may match zero, one, or multiple rules.
- The request is allowed if and only if all matching rules have capacity for this client (i.e., the client has made strictly fewer than
limitrequests within the window(timestamp - window_seconds, timestamp]for every matched rule). - If the request is allowed, it consumes 1 quota unit under all matching rules for that
client_id. - If the request is denied, it does not consume quota under any rule.
- If no rules match the request, it is allowed by default.
- A request matches a rule if
Assume timestamps are provided in non-decreasing order across successive calls to allow_request.
Interface
class RateLimiter:
def __init__(self):
pass
def add_rule(self, rule_id: str, path_prefix: str, limit: int, window_seconds: int) -> None:
pass
def allow_request(self, timestamp: int, client_id: str, path: str) -> bool:
pass
Example
limiter = RateLimiter()
limiter.add_rule("api_general", "/api", limit=2, window_seconds=10)
limiter.add_rule("api_auth", "/api/auth", limit=1, window_seconds=5)
# t=1: matches both "api_general" and "api_auth"
limiter.allow_request(1, "client_1", "/api/auth/login") # returns True
# t=2: matches both. "api_general" has 1/2, but "api_auth" has 1/1 (full)
limiter.allow_request(2, "client_1", "/api/auth/login") # returns False (quota not consumed)
# t=3: matches only "api_general". Currently at 1/2
limiter.allow_request(3, "client_1", "/api/data") # returns True (now at 2/2)
# t=7: matches both.
# "api_auth" window is (2, 7]: request at t=1 has expired (0/1 used).
# "api_general" window is (0, 7]: requests at t=1 and t=3 are active (2/2 used).
limiter.allow_request(7, "client_1", "/api/auth/login") # returns False
Constraints
- Total calls to
allow_request<=100,000 - Total calls to
add_rule<=100 1 <= limit <= 10,0001 <= window_seconds <= 86,400timestampis a non-negative integer, monotonically non-decreasing- Strings
rule_id,client_id,path, andpath_prefixare non-empty ASCII strings of length <=100
Hints
Hint 1
Store client request timestamps using a double-ended queue (deque) keyed by (rule_id, client_id).
Hint 2
Because timestamps are non-decreasing, you can purge expired timestamps from the front of the deque in O(1) amortized time.
Hint 3
Ensure atomicity: verify that every matched rule has available capacity before appending the current timestamp to any rule's history.
Follow-up questions
- How would you optimize rule matching if there were 100,000 path prefixes instead of 100?
- If multiple instances of this service run across distributed edge nodes, how would you maintain consistent state without adding high latency?
- How would you handle client memory cleanup for inactive client IDs to prevent an unbounded memory leak?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.