Katkat

Build a Dual Fixed-Window Rate-Limiting Middleware

HeadwaySoftware Engineer · Onsite

Last updated

Tests your ability to design and implement a thread-safe, dual fixed-window rate limiter and integrate it cleanly as HTTP middleware in an asynchronous Python framework.

Problem Statement

You are building an API gateway middleware for a FastAPI application running on Uvicorn. To protect downstream dependencies and ensure fair resource allocation, incoming requests must satisfy two independent rate limits simultaneously:

  1. Global Limit: No more than 100 requests per 60-second window across all clients.
  2. Per-Domain Limit: No more than 3 requests per 1-second window for any specific client domain (extracted from the Host header or client identity).

If either limit is exceeded, the middleware must immediately reject the request with HTTP status 429 Too Many Requests without forwarding the call to downstream handlers. If the request is permitted, both the global counter and the domain-specific counter must be incremented atomically.

Implement the rate-limiting core logic and the FastAPI middleware.

Requirements

  1. Use a fixed-window counter approach where the window boundaries are aligned with epoch time: window_id = int(timestamp // window_size_seconds).
  2. When a window rolls over, past counts should be discarded or reset lazily to prevent unbounded memory growth.
  3. Make your implementation thread-safe / concurrency-safe against asynchronous tasks running on the same process.
  4. If rejected, return a meaningful header or response indicating which limit was breached (e.g., "Global rate limit exceeded" vs "Domain rate limit exceeded").

Interfaces

class DualFixedWindowRateLimiter:
    def __init__(
        self,
        global_limit: int = 100,
        global_window_sec: int = 60,
        domain_limit: int = 3,
        domain_window_sec: int = 1,
    ):
        """Initialize counters, locks, and window configurations."""
        pass

    def is_allowed(self, domain: str, now: float | None = None) -> tuple[bool, str | None]:
        """
        Check and consume allowance for a request.
        
        Returns:
            (True, None) if the request is permitted.
            (False, reason) if rejected, where reason is 'global' or 'domain'.
        """
        pass

Examples

Example 1: Normal throughput within limits

  • t = 10.00: Domain api.partner-a.com sends 3 requests -> All 3 return (True, None).
  • t = 10.50: Domain api.partner-a.com sends a 4th request -> Returns (False, 'domain') because 3 req/sec is exceeded.
  • t = 11.01: Domain api.partner-a.com sends a request -> Returns (True, None) because a new 1-second window has started.

Example 2: Global limit saturation

  • Over 60 seconds, 100 requests are distributed across 50 distinct domains (2 requests each, well within the 3 req/sec limit).
  • The 101st request arrives at t = 59.9 from any domain -> Returns (False, 'global').

Constraints & Edge Cases

  • Concurrent async requests running concurrently via asyncio.
  • Missing or malformed Host header (treat as "unknown" or reject early).
  • Memory leaks from inactive domains: stale domain windows must be cleaned up periodically or pruned when evaluated.

Hints

Hint 1

Compute the current window key as `int(now // window_seconds)` so that counter resets happen implicitly whenever the key changes.

Hint 2

Evaluate both limits before incrementing counters, or ensure that if the second limit check fails, the first limit's increment is rolled back.

Hint 3

Use an `asyncio.Lock` or standard `threading.Lock` depending on whether state access is synchronous or involves async I/O, ensuring atomicity across concurrent requests.

Follow-up questions

  • How does the fixed-window algorithm behave at boundary edges (the 'burst at the boundary' problem), and how would you transition to a sliding window or token bucket?
  • If your FastAPI service scales horizontally to 10 Uvicorn workers or across multiple Kubernetes pods, how would you adapt this state management using Redis?
  • How would you implement an eviction strategy for inactive domain keys without degrading request latency under heavy load?