Roblox logoRoblox
Coding·45 minFree preview

Rate Limiter (Sliding Window) with Per-Entity Follow-up

The single most common Roblox phone-screen / OA coding problem: implement a sliding-window rate limiter that decides, request by request, whether each timestamped request would breach a `maxRequests` cap within a fixed `windowLength`. The follow-up generalizes the cap to be enforced independently per user and per experience (game), denying a request if either limit is exceeded.

SWE
MLE
Infra Eng
rate-limiting
sliding-window
hashmap
queue
medium
Frequency
High
Last asked
2026-07-09
Stage
phone-screen · oa · onsite-coding

Problem Overview

Implement a rate limiter over a stream of request timestamps. The interview usually starts with a single global stream, then adds user and experience identifiers. A request is allowed only if accepting it would not exceed the configured request count inside the sliding window.

Part 1: Global Sliding-Window Limiter

Problem Statement

Given sorted request timestamps, a window length, and a maximum request count, return whether each request should be accepted.

from typing import List

def rate_limiter(
    requestTimestamps: List[int],
    windowLength: int,
    maxRequests: int,
) -> List[bool]:
    """
    Return True for each accepted request and False for each denied request.
    Only accepted requests count against future capacity.
    """
    pass

Example

requestTimestamps = [1, 2, 3, 4, 5, 6]
windowLength = 3
maxRequests = 2

rate_limiter(requestTimestamps, windowLength, maxRequests)
# [True, True, False, True, True, False]

Walkthrough:

RequestAccepted timestamps still in windowDecision
t = 1[]Accept
t = 2[1]Accept
t = 3[1, 2]Deny
t = 4[2] after removing 1Accept
t = 5[4] after removing 2Accept
t = 6[4, 5]Deny

Solution

Use a queue of accepted timestamps. Before processing timestamp t, remove every accepted timestamp that is no longer in the open-left window (t - windowLength, t]. If the queue size is below maxRequests, accept and append t; otherwise deny and do not append.

from collections import deque
from typing import List

def rate_limiter(
    requestTimestamps: List[int],
    windowLength: int,
    maxRequests: int,
) -> List[bool]:
    accepted = deque()
    decisions = []

    for timestamp in requestTimestamps:
        cutoff = timestamp - windowLength
        while accepted and accepted[0] <= cutoff:
            accepted.popleft()

        if len(accepted) < maxRequests:
            decisions.append(True)
            accepted.append(timestamp)
        else:
            decisions.append(False)

    return decisions

Complexity:

  • Time: O(n), because each accepted timestamp is appended once and removed once.
  • Space: O(k), where k is the number of accepted timestamps still inside the active window. Since denied requests are not stored, this is at most maxRequests for Part 1.

Part 2: Per-User and Per-Experience Limits

Problem Statement

Now each request has a userId and an experienceId. Enforce an independent sliding-window limit for every user and every experience. A request is accepted only if both the user limiter and the experience limiter have capacity.

from typing import List

def per_entity_rate_limiter(
    requestTimestamps: List[int],
    userIds: List[int],
    experienceIds: List[str],
    windowLength: int,
    maxRequests: int,
) -> List[bool]:
    """
    Return True when the request is allowed by both:
    1. its user's sliding window
    2. its experience's sliding window
    """
    pass

Example

requestTimestamps = [1, 2, 3, 4, 5]
userIds = [1, 1, 2, 1, 2]
experienceIds = ["A", "A", "A", "A", "B"]
windowLength = 3
maxRequests = 1

per_entity_rate_limiter(
    requestTimestamps,
    userIds,
    experienceIds,
    windowLength,
    maxRequests,
)
# [True, False, False, True, True]

Walkthrough:

RequestReason
(t=1, user=1, exp=A)First request for both entities, accept.
(t=2, user=1, exp=A)User 1 already has an accepted request in the window, deny.
(t=3, user=2, exp=A)Experience A already has an accepted request in the window, deny.
(t=4, user=1, exp=A)The accepted t=1 request is expired, accept.
(t=5, user=2, exp=B)User 2 has no accepted request, and B is new, accept.

Solution

Use two maps:

  • user_id -> deque[accepted_timestamps]
  • experience_id -> deque[accepted_timestamps]

For each request, clean both queues first. Then check both counts. Only append the timestamp to both queues if the request is accepted.

from collections import defaultdict, deque
from typing import Deque, Dict, List

def _cleanup(window: Deque[int], timestamp: int, windowLength: int) -> None:
    cutoff = timestamp - windowLength
    while window and window[0] <= cutoff:
        window.popleft()

def per_entity_rate_limiter(
    requestTimestamps: List[int],
    userIds: List[int],
    experienceIds: List[str],
    windowLength: int,
    maxRequests: int,
) -> List[bool]:
    user_windows: Dict[int, Deque[int]] = defaultdict(deque)
    experience_windows: Dict[str, Deque[int]] = defaultdict(deque)
    decisions = []

    for timestamp, user_id, experience_id in zip(
        requestTimestamps,
        userIds,
        experienceIds,
    ):
        user_window = user_windows[user_id]
        experience_window = experience_windows[experience_id]

        _cleanup(user_window, timestamp, windowLength)
        _cleanup(experience_window, timestamp, windowLength)

        if len(user_window) < maxRequests and len(experience_window) < maxRequests:
            decisions.append(True)
            user_window.append(timestamp)
            experience_window.append(timestamp)
        else:
            decisions.append(False)

    return decisions

Complexity:

  • Time: O(n) amortized. Each accepted request can be removed once from its user queue and once from its experience queue.
  • Space: O(a), where a is the number of accepted requests still active across entity windows. Each accepted request is stored in two queues.

Follow-up Variant: More Request Fields

Some follow-ups add fields such as IP address, device ID, or several arbitrary request attributes and ask how you would rate-limit separately by each field.

The pattern is the same: create one map of queues per dimension. A request is accepted only if every dimension-specific queue has capacity.

from collections import defaultdict, deque
from typing import Deque, Dict, Hashable, List, Mapping

def multi_field_rate_limiter(
    requestTimestamps: List[int],
    fieldsByName: Mapping[str, List[Hashable]],
    windowLength: int,
    maxRequests: int,
) -> List[bool]:
    windows: Dict[str, Dict[Hashable, Deque[int]]] = {
        field_name: defaultdict(deque)
        for field_name in fieldsByName
    }
    decisions = []

    for i, timestamp in enumerate(requestTimestamps):
        current_windows = []

        for field_name, values in fieldsByName.items():
            window = windows[field_name][values[i]]
            _cleanup(window, timestamp, windowLength)
            current_windows.append(window)

        if all(len(window) < maxRequests for window in current_windows):
            decisions.append(True)
            for window in current_windows:
                window.append(timestamp)
        else:
            decisions.append(False)

    return decisions

Adding IP limits becomes a data change:

multi_field_rate_limiter(
    requestTimestamps,
    {
        "user": userIds,
        "experience": experienceIds,
        "ip": ipAddresses,
    },
    windowLength,
    maxRequests,
)

If the interviewer wants different limits per field, store a config per dimension:

limits = {
    "user": (60, 100),       # windowLength, maxRequests
    "experience": (60, 500),
    "ip": (60, 50),
}

Then clean and compare each queue with its own (windowLength, maxRequests) pair.

Test Cases

assert rate_limiter([1, 2, 3, 4, 5, 6], 3, 2) == [
    True, True, False, True, True, False
]

# Requests exactly on the left boundary expire.
assert rate_limiter([1, 4], 3, 1) == [True, True]

# Denied requests are not stored.
assert rate_limiter([1, 2, 3, 4], 3, 2) == [True, True, False, True]

assert per_entity_rate_limiter(
    [1, 2, 3, 4, 5],
    [1, 1, 2, 1, 2],
    ["A", "A", "A", "A", "B"],
    3,
    1,
) == [True, False, False, True, True]

# Experience cap can deny a request even when the user is new.
assert per_entity_rate_limiter(
    [10, 11],
    [1, 2],
    ["game-1", "game-1"],
    10,
    1,
) == [True, False]
Was this article helpful?

Comments

Sign in to join the discussion
Loading...