Implement Layered Endpoint and Global Rate Limits
HeadwaySoftware Engineer · Technical ScreenLast updated
This problem tests your ability to design a thread-safe, memory-efficient rate limiter that manages hierarchical quotas using a rolling window mechanism.
Problem Statement
Design a rate limiter class RateLimiter that enforces two layers of limits for incoming API requests: a global limit (across all endpoints) and an endpoint-specific limit.
Implement the following interface:
class RateLimiter:
def __init__(self, global_limit: int, endpoint_limits: dict[str, int], window_seconds: int):
"""
:param global_limit: Max requests allowed globally per window_seconds.
:param endpoint_limits: Map of endpoint paths to their max requests per window_seconds.
:param window_seconds: The duration of the rolling window in seconds.
"""
def allow_request(self, endpoint: str) -> bool:
"""
Returns True if the request is allowed, False otherwise.
Must be thread-safe.
"""
Requirements
- Atomic enforcement: A request must satisfy both the global limit and the specific endpoint limit to be allowed. If either is exceeded, the request must be rejected.
- Rolling Window: The limits are based on a sliding window of time. For example, if
window_secondsis 60, a request at timeTchecks the count of requests in[T - 60, T]. - Memory efficiency: The state storage should not grow indefinitely. Clean up expired request timestamps.
Example
global_limit= 100,window_seconds= 10endpoint_limits= {"/login": 10}- A call to
allow_request("/login")att=5is allowed if the count of all requests in[-5, 5]is < 100 AND the count of "/login" requests is < 10.
Constraints
- Assume
window_secondsis a positive integer. - The solution must be thread-safe for concurrent calls to
allow_request. - Minimize lock contention.
Hints
Hint 1
Consider using a queue or deque for each counter to store individual request timestamps, allowing you to prune timestamps outside the current window.
Hint 2
Using a single global lock will be simple to implement but may become a bottleneck; consider using fine-grained locks or atomic primitives.
Hint 3
For pruning, you can lazily remove outdated timestamps at the start of each `allow_request` call to keep memory usage proportional to the request rate.
Follow-up questions
- How would you modify this design if the rate limiter needed to be distributed across multiple server instances using Redis?
- What are the trade-offs between using a sliding window vs. a fixed-window (token bucket) algorithm in terms of accuracy and performance?
- How would you handle 'bursty' traffic? Should we allow a small amount of token 'carry-over' between windows?
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.