Reason About Rate Limiting Under Memory and Throughput Pressure
PlaidSoftware Engineer · OnsiteLast updated
Tests the candidate's ability to design a high-throughput, memory-bounded rate limiter using bucketed counters instead of unbounded timestamp queues, balancing concurrency, memory overhead, and precision.
Problem Statement
At high request volumes, maintaining an exact sliding-window rate limiter by storing individual request timestamps consumes O(R) memory (where R is the number of requests in the window). Under heavy load (e.g., 50,000 requests per second per client), this approach leads to prohibitive memory usage and severe garbage collection pressure.
Design an in-memory, bounded-memory rate limiter that supports sliding window semantics using a bucketed sliding window counter. Instead of storing raw timestamps, the limiter divides the time window into B fixed-size sub-buckets, bounding the memory footprint to O(B) per client key regardless of request volume.
Implement the SlidingWindowCounterLimiter class with the following methods:
class SlidingWindowCounterLimiter:
def __init__(self, max_requests: int, window_seconds: int, bucket_count: int):
"""
max_requests: Maximum allowed requests within any rolling window.
window_seconds: Total duration of the rate limit window in seconds.
bucket_count: Number of sub-buckets to slice the window into (controls precision).
"""
pass
def allow(self, key: str, current_time_ms: int) -> bool:
"""
Determines whether a request with the given key at current_time_ms is permitted.
If permitted, increments the count and returns True; otherwise, returns False.
"""
pass
Approximation Logic
- The duration of each sub-bucket is
bucket_duration_ms = (window_seconds * 1000) // bucket_count. - Upon a request at
current_time_ms, expire and clear any buckets older thanwindow_seconds * 1000ms fromcurrent_time_ms. - Compute the total active requests across the valid buckets.
- If
active_requests < max_requests, record the request in the bucket corresponding tocurrent_time_msand returnTrue. Otherwise, returnFalse.
Example
limiter = SlidingWindowCounterLimiter(max_requests=3, window_seconds=10, bucket_count=5)
# Each bucket represents (10 * 1000) / 5 = 2000 ms (2 seconds)
limiter.allow("client_a", 1000) # True, bucket [0, 2000), total = 1
limiter.allow("client_a", 1500) # True, bucket [0, 2000), total = 2
limiter.allow("client_a", 3000) # True, bucket [2000, 4000), total = 3
limiter.allow("client_a", 3500) # False, total = 3 (limit reached)
limiter.allow("client_a", 11500) # True, bucket [0, 2000) expired (> 10s old), total = 1 + 1 = 2 < 3
Constraints & Edge Cases
1 <= max_requests <= 10^91 <= window_seconds <= 36001 <= bucket_count <= 100- Requests are generally monotonic in time per worker, but small out-of-order arrivals within clock skew tolerances (e.g., within the current bucket) must not crash the state.
- Consider memory reclamation: idle keys must not leak memory indefinitely.
Hints
Hint 1
A circular array (ring buffer) of size `bucket_count` paired with a timestamp or bucket index allows O(1) bucket expiration without resizing.
Hint 2
To avoid high lock contention on a single shared state, partition keys across striped locks or shards using consistent hashing.
Hint 3
If an incoming request's timestamp is far beyond the window, clear all stale buckets rather than iterating through skipped intervals one by one.
Follow-up questions
- How would you modify the bucket aggregation to interpolate the weight of the oldest partial bucket to further reduce boundary overshoot without increasing bucket count?
- If a single hot key receives millions of requests per second, how do you prevent lock contention from degrading throughput across unrelated keys?
- Under severe global memory pressure, how would you transition from per-key bucketed counters to probabilistic structures like Count-Min Sketch or Heavy Hitters?
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.