Katkat

Implement Layered Endpoint and Global Rate Limits

HeadwaySoftware Engineer · Technical Screen

Last 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

  1. 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.
  2. Rolling Window: The limits are based on a sliding window of time. For example, if window_seconds is 60, a request at time T checks the count of requests in [T - 60, T].
  3. Memory efficiency: The state storage should not grow indefinitely. Clean up expired request timestamps.

Example

  • global_limit = 100, window_seconds = 10
  • endpoint_limits = {"/login": 10}
  • A call to allow_request("/login") at t=5 is allowed if the count of all requests in [-5, 5] is < 100 AND the count of "/login" requests is < 10.

Constraints

  • Assume window_seconds is 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?