Implement a sliding-window rate limiter
Company: Roblox
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: Evaluates the candidate's ability to design and implement an in-memory sliding-window rate limiter enforcing per-user and per-game constraints, focusing on correct request-acceptance semantics and efficient timestamp eviction; Category/domain: Coding & Algorithms; Level of abstraction: component-level algorithm and data-structure implementation.
Part 1: Per-user Sliding-Window Rate Limiter
Constraints
- 0 <= limit <= 10^5
- 1 <= window_ms <= 10^9
- 0 <= len(requests) <= 2 * 10^5
- 0 <= timestamp_ms <= 10^12
- requests are sorted by non-decreasing timestamp_ms
- user_id is a hashable identifier such as a string or integer
Examples
Input: (2, 1000, [('u1', 0), ('u1', 500), ('u1', 999), ('u1', 1000), ('u1', 1500)])
Expected Output: [True, True, False, True, True]
Explanation: At time 999, user u1 already has allowed requests at 0 and 500 within the window [0, 999], so the third request is rejected. At time 1000, timestamp 0 expires because the new window is [1, 1000].
Input: (1, 10, [('a', 1), ('b', 1), ('a', 5), ('b', 10), ('a', 11)])
Expected Output: [True, True, False, False, True]
Explanation: Each user has an independent limiter. User a is blocked at time 5 because time 1 is still in the window [ -4, 5 ] conceptually, or [1, 5] after clamping to seen timestamps. User b is blocked at time 10 because its earlier request at 1 is still inside [1, 10].
Input: (3, 100, [])
Expected Output: []
Explanation: Edge case: no requests.
Input: (2, 1, [('x', 100), ('x', 100), ('x', 101)])
Expected Output: [True, True, True]
Explanation: With a 1 ms window, only requests from the exact same millisecond count together. Both requests at 100 are allowed because the limit is 2. At 101, the two requests at 100 have expired.
Hints
- A deque is useful because old timestamps expire from the left, while new accepted timestamps are appended to the right.
- To avoid stale data for users who may not appear again, keep a global deque of accepted events and evict expired events from both the global structure and the matching user's deque.
Part 2: Combined Per-user and Per-game Sliding-Window Rate Limiter
Constraints
- 0 <= user_limit <= 10^5
- 0 <= game_limit <= 10^5
- 1 <= window_ms <= 10^9
- 0 <= len(requests) <= 2 * 10^5
- 0 <= timestamp_ms <= 10^12
- requests are sorted by non-decreasing timestamp_ms
- user_id and game_id are hashable identifiers such as strings or integers
Examples
Input: (2, 1, 100, [('u1', 'g1', 0), ('u2', 'g1', 10), ('u1', 'g2', 20), ('u3', 'g1', 50), ('u1', 'g1', 101)])
Expected Output: [True, False, True, False, True]
Explanation: Game g1 allows only 1 request per 100 ms window, so the requests at 10 and 50 are rejected because g1 already accepted the request at 0. By time 101, timestamp 0 has expired, so the last request is allowed.
Input: (1, 1, 10, [('u1', 'g1', 1), ('u2', 'g1', 2), ('u2', 'g2', 3), ('u2', 'g2', 4), ('u1', 'g2', 11)])
Expected Output: [True, False, True, False, False]
Explanation: The second request is rejected by the game limiter and must not count against user u2. That is why u2 can still be accepted at time 3 on a different game.
Input: (1, 2, 5, [('a', 'x', 0), ('a', 'y', 4), ('a', 'x', 5), ('b', 'x', 5), ('a', 'x', 9)])
Expected Output: [True, False, True, True, False]
Explanation: At time 5, the request at time 0 has expired because the active window is [1, 5]. Later, at time 9, user a already has an accepted request at 5 in the window [5, 9], so the request is rejected.
Input: (3, 3, 50, [])
Expected Output: []
Explanation: Edge case: no requests.
Hints
- You need one queue per user and one queue per game, but be careful: a rejected request must be added to neither queue.
- A global queue of accepted events lets you evict expired timestamps for inactive users or games, so memory only tracks data that can still matter.