Implement queue-based rate limiter with multi-key limits
Company: Roblox
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Onsite
Quick Answer: This question evaluates implementation and design skills around rate limiting, including rolling-window counters, multi-key constraints, and atomic updates, and falls under the Coding & Algorithms domain focused on practical application of algorithms and data structures.
Part 1: Rolling Window Queue Rate Limiter
Constraints
- 1 <= K <= 10^5
- 1 <= W <= 10^9
- 0 <= len(timestamps) <= 2 * 10^5
- 0 <= timestamps[i] <= 10^9
- timestamps is sorted in nondecreasing order
Examples
Input: (2, 5, [1, 2, 3, 6, 7])
Expected Output: [True, True, False, True, True]
Explanation: At time 3, the allowed requests at 1 and 2 are still inside the last 5 ms, so the third request is denied. By time 6, timestamp 1 has expired.
Input: (1, 3, [5, 5, 8, 8])
Expected Output: [True, False, True, False]
Explanation: Requests at the same timestamp count toward the same window. A request at time 5 expires when the current time reaches 8.
Input: (3, 10, [])
Expected Output: []
Explanation: Edge case: no requests.
Input: (3, 10, [100])
Expected Output: [True]
Explanation: Edge case: a single request is allowed.
Input: (2, 1, [0, 0, 1, 1])
Expected Output: [True, True, True, True]
Explanation: With W = 1, timestamps equal to current_time - 1 are expired, so both requests at time 0 are gone before processing time 1.
Solution
def solution(K, W, timestamps):
from collections import deque
q = deque()
result = []
for t in timestamps:
while q and q[0] <= t - W:
q.popleft()
if len(q) < K:
q.append(t)
result.append(True)
else:
result.append(False)
return resultTime complexity: O(n), where n is the number of timestamps. Each allowed timestamp is appended once and removed once.. Space complexity: O(K) in the worst case, because the queue never holds more than K active allowed requests..
Hints
- Keep only allowed request timestamps that are still inside the current rolling window.
- A deque is a natural fit because expired timestamps always leave from the front.
Part 2: Atomic Multi-Key Rate Limiter
Constraints
- 1 <= Ku, Ke <= 10^5
- 1 <= W <= 10^9
- 0 <= len(requests) <= 2 * 10^5
- userId and experience are strings
- 0 <= timestamp <= 10^9
- request timestamps are sorted in nondecreasing order
Examples
Input: (1, 2, 5, [('u1', 'A', 1), ('u1', 'A', 2), ('u2', 'A', 2)])
Expected Output: [True, False, True]
Explanation: The second request is blocked by the per-user limit. Because the update is atomic, it does not consume experience A capacity, so the third request is still allowed.
Input: (2, 1, 4, [('u1', 'A', 1), ('u2', 'A', 2), ('u2', 'B', 2), ('u3', 'A', 5)])
Expected Output: [True, False, True, True]
Explanation: The second request is denied by the per-experience limit for A. At time 5, the earlier A request at time 1 has expired.
Input: (2, 2, 10, [])
Expected Output: []
Explanation: Edge case: empty request list.
Input: (2, 2, 3, [('u1', 'E1', 0), ('u1', 'E2', 0), ('u1', 'E3', 0), ('u2', 'E1', 0), ('u2', 'E1', 3)])
Expected Output: [True, True, False, True, True]
Explanation: u1 reaches its per-user limit after the first two requests, so the third is denied. At time 3, all timestamp 0 entries have expired because W = 3.
Input: (1, 1, 1, [('u1', 'X', 10), ('u1', 'X', 10), ('u1', 'X', 11)])
Expected Output: [True, False, True]
Explanation: Boundary case: the allowed request at time 10 expires exactly when processing time 11.
Solution
def solution(Ku, Ke, W, requests):
from collections import deque
user_queues = {}
exp_queues = {}
result = []
for user_id, experience, timestamp in requests:
user_q = user_queues.get(user_id)
if user_q is not None:
while user_q and user_q[0] <= timestamp - W:
user_q.popleft()
if not user_q:
del user_queues[user_id]
user_q = None
exp_q = exp_queues.get(experience)
if exp_q is not None:
while exp_q and exp_q[0] <= timestamp - W:
exp_q.popleft()
if not exp_q:
del exp_queues[experience]
exp_q = None
user_count = 0 if user_q is None else len(user_q)
exp_count = 0 if exp_q is None else len(exp_q)
if user_count < Ku and exp_count < Ke:
if user_q is None:
user_q = deque()
user_queues[user_id] = user_q
if exp_q is None:
exp_q = deque()
exp_queues[experience] = exp_q
user_q.append(timestamp)
exp_q.append(timestamp)
result.append(True)
else:
result.append(False)
return resultTime complexity: O(n) amortized total time, where n is the number of requests. Each allowed timestamp is appended once and removed once from its user queue and once from its experience queue.. Space complexity: O(a + u + e) for active data, where a is the number of allowed requests still inside the window, and u and e are the numbers of active userId and experience keys. This is O(n) in the worst case. Deleting empty deques after pruning garbage-collects stale keys..
Hints
- Maintain one hash map of deques for userId counts and another for experience counts.
- Prune expired timestamps first, then check both limits, and append to both deques only if both checks pass.