Implement a Rolling-Window Rate Limiter
RobloxSoftware Engineer · Technical ScreenLast updated
Implement an in-memory, rolling-window rate limiter supporting per-client limits, precise boundary expiration, and stale state cleanup.
Problem Statement
Implement an in-memory sliding-window log rate limiter. The rate limiter restricts the number of requests each client can make within a specified floating-point time window. Only accepted requests consume capacity; rejected requests must not be counted against the client's limit.
Implement the RollingWindowRateLimiter class with the following methods:
__init__(self, max_requests: int, window_size_seconds: float): Initializes the rate limiter with the maximum number of allowed requests and the window length in seconds.allow_request(self, client_id: str, timestamp: float) -> bool: Checks if a request fromclient_idattimestampis permitted. A previous request att_previs within the active window iftimestamp - t_prev < window_size_seconds. If the number of active requests is strictly less thanmax_requests, the request is accepted, recorded, and returnsTrue. Otherwise, it returnsFalsewithout recording the timestamp.cleanup_inactive(self, current_time: float) -> int: Evicts all tracked clients whose active request count is zero relative tocurrent_time(i.e., all their past requests have expired). Returns the number of client records deleted from internal state.
Example
limiter = RollingWindowRateLimiter(max_requests=2, window_size_seconds=10.0)
# Client 1 makes requests
assert limiter.allow_request("user_1", 1.0) == True # 1 active [1.0]
assert limiter.allow_request("user_1", 5.0) == True # 2 active [1.0, 5.0]
assert limiter.allow_request("user_1", 8.0) == False # Rejected; state remains [1.0, 5.0]
# At timestamp 11.0, 11.0 - 1.0 = 10.0 >= 10.0, so 1.0 expires
assert limiter.allow_request("user_1", 11.0) == True # Active: [5.0, 11.0]
# Client 2 makes an independent request
assert limiter.allow_request("user_2", 12.0) == True # Active: [12.0]
# Cleanup at timestamp 22.0
# For user_1: 22.0 - 11.0 = 11.0 >= 10.0 (all expired)
# For user_2: 22.0 - 12.0 = 10.0 >= 10.0 (all expired)
assert limiter.cleanup_inactive(22.0) == 2
Constraints
1 = 0.0- Timestamps passed to
allow_requestfor any givenclient_idare guaranteed to be monotonically non-decreasing. client_idis a non-empty string.
Edge Cases
- Exact Window Boundary: A request occurring at
t_prevwheretimestamp - t_prev == window_size_secondsis considered expired. - Rejected Requests: Must never be stored; if rejected requests were appended, a client could DOS themselves indefinitely.
- Zero-Active Clients: Clients that have expired entries should be lazily pruned during
allow_requestcalls and eagerly removed duringcleanup_inactive.
Hints
Hint 1
Use a hash map mapping each client ID to a double-ended queue (`collections.deque`) of accepted timestamps.
Hint 2
Before deciding whether to accept a new request, pop timestamps from the front of the client's deque that fall outside `timestamp - window_size_seconds`.
Hint 3
Ensure rejected requests are not added to the deque, and delete the client key during cleanup if their deque becomes empty after popping expired entries.
Follow-up questions
- How would you ensure thread safety if `allow_request` is called concurrently across multiple worker threads in Python?
- If scaling horizontally across multiple machines, how would you implement this rate limiter using Redis without race conditions?
- Sliding window logs require storing every timestamp. How does a sliding window counter approximation trade off accuracy for memory?
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.