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]
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]
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]
Input: (3, 3, 50, [])
Expected Output: []
Input: (0, 5, 100, [('u1', 'g1', 0), ('u2', 'g2', 1)])
Expected Output: [false, false]
Input: (1, 5, 10, [('u1', 'g1', 0), ('u1', 'g1', 10), ('u1', 'g1', 11)])
Expected Output: [true, true, false]
Input: (100, 2, 1000, [('u1', 'g1', 0), ('u2', 'g1', 0), ('u3', 'g1', 0)])
Expected Output: [true, true, false]
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.