Orivel Orivel
Open menu

Rate Limiter with Sliding Window and Fair Multi-Tenant Quotas

Compare model answers for this Coding benchmark and review scores, judging comments, and related examples.

Login or register to use likes and favorites. Register

X f L

Contents

Task Overview

Benchmark Genres

Coding

Task Creator Model

Answering Models

Judge Models

Task Prompt

Implement a reusable rate limiter library in a language of your choice (Python, Go, TypeScript, Java, or Rust) that enforces per-client request quotas using a sliding-window algorithm, plus a fair-sharing policy across multiple tenants.

Functional requirements:

  1. Provide a class or module with a method such as allow(tenant_id, client_id, now_ms) that returns whether a request is permitted and, when denied, how many milliseconds until the next request would be allowed (retry_after_ms).
  2. Each client is limited to...
Show more

Implement a reusable rate limiter library in a language of your choice (Python, Go, TypeScript, Java, or Rust) that enforces per-client request quotas using a sliding-window algorithm, plus a fair-sharing policy across multiple tenants.

Functional requirements:

  1. Provide a class or module with a method such as allow(tenant_id, client_id, now_ms) that returns whether a request is permitted and, when denied, how many milliseconds until the next request would be allowed (retry_after_ms).
  2. Each client is limited to a maximum number of requests within a rolling time window (for example, 100 requests per 60,000 ms). Configuration must be adjustable per tenant.
  3. Implement a true sliding window (weighted or log-based), not a fixed calendar-bucket window, so that bursts across bucket boundaries are handled correctly.
  4. Add a per-tenant global cap so that all clients of a tenant combined cannot exceed a tenant-level ceiling, and when the tenant is saturated, remaining capacity is shared fairly across active clients rather than being monopolized by one client.
  5. The limiter must be safe under concurrent access from multiple threads or async tasks.
  6. Memory must not grow unbounded: stale client state must be evicted or compacted over time.

Deliverables:

  • The complete implementation with clear public API and inline documentation of key decisions.
  • A brief explanation (in comments or a short prose section) of the sliding-window algorithm you chose and its accuracy/memory tradeoffs.
  • A test suite covering the core edge cases described below.

Edge cases to address explicitly in code and tests:

  • Requests exactly at the window boundary.
  • A client that goes idle then returns after the window has fully elapsed.
  • Concurrent requests racing against the same client counter.
  • Clock going backwards or duplicate timestamps.
  • Tenant saturation and fair redistribution among competing clients.
  • Eviction of stale client state without dropping active clients.

State any assumptions you make (single-process vs distributed, monotonic clock availability, etc.). If you assume a single process, briefly describe how the design would extend to a distributed deployment.

Judging Policy

A strong answer delivers correct, runnable code with a clean public API and clearly enforces both per-client sliding-window limits and per-tenant global caps. Judges should reward a genuine sliding-window implementation (weighted counter or timestamp log) rather than a naive fixed-bucket approximation, and should check that the retry_after_ms value is computed sensibly. Fair redistribution of remaining tenant capacity among active clients when the tenant is saturated is an important differentiator; solutions that o...

Show more

A strong answer delivers correct, runnable code with a clean public API and clearly enforces both per-client sliding-window limits and per-tenant global caps. Judges should reward a genuine sliding-window implementation (weighted counter or timestamp log) rather than a naive fixed-bucket approximation, and should check that the retry_after_ms value is computed sensibly. Fair redistribution of remaining tenant capacity among active clients when the tenant is saturated is an important differentiator; solutions that only implement independent per-client limits are incomplete. Concurrency safety must be handled correctly (locks, atomics, or an actor/single-loop model) without obvious race conditions, and the design must bound memory via eviction or compaction of stale state. The stated edge cases should be addressed both in the code logic and in tests: boundary timing, idle-then-return behavior, concurrent races, backwards or duplicate clock values, tenant saturation, and safe eviction. Higher-quality answers explain the algorithm's accuracy/memory tradeoffs, state assumptions explicitly, and note how the design would extend to a distributed setting. Deduct for incorrect boundary handling, unbounded memory growth, missing concurrency protection, tests that do not actually exercise the claimed edge cases, or code that would not run as written.

Answers

from future import annotations

import threading
import unittest
from collections import deque
from dataclasses import dataclass, field
from typing import Deque, Dict, Hashable, Optional

@dataclass(frozen=True)
class TenantConfig:
"""Rate limits for one tenant.

client_limit applies independently to each client, while tenant_limit applies
to all admitted requests from the tenant. Both limits use exact rolling
windows. active_timeout_ms bounds how long an absent fairness waiter can
retain its turn. eviction_ms controls client-state retention.
"""

client_limit: int
client_wind...
Show Full Answer

from future import annotations

import threading
import unittest
from collections import deque
from dataclasses import dataclass, field
from typing import Deque, Dict, Hashable, Optional

@dataclass(frozen=True)
class TenantConfig:
"""Rate limits for one tenant.

client_limit applies independently to each client, while tenant_limit applies
to all admitted requests from the tenant. Both limits use exact rolling
windows. active_timeout_ms bounds how long an absent fairness waiter can
retain its turn. eviction_ms controls client-state retention.
"""

client_limit: int
client_window_ms: int
tenant_limit: int
tenant_window_ms: int
active_timeout_ms: int = 60_000
eviction_ms: int = 120_000

def validate(self) -> None:
    if self.client_limit <= 0 or self.tenant_limit <= 0:
        raise ValueError("limits must be positive")
    if self.client_window_ms <= 0 or self.tenant_window_ms <= 0:
        raise ValueError("window lengths must be positive")
    if self.active_timeout_ms <= 0:
        raise ValueError("active_timeout_ms must be positive")
    if self.eviction_ms < max(self.client_window_ms, self.tenant_window_ms):
        raise ValueError("eviction_ms must be at least as large as both windows")
    if self.eviction_ms < self.active_timeout_ms:
        raise ValueError("eviction_ms must be at least active_timeout_ms")

@dataclass(frozen=True)
class AllowResult:
allowed: bool
retry_after_ms: int = 0

@dataclass
class _ClientState:
admitted: Deque[int] = field(default_factory=deque)
last_seen_ms: int = 0

@dataclass
class _TenantState:
admitted: Deque[int] = field(default_factory=deque)
clients: Dict[Hashable, _ClientState] = field(default_factory=dict)
waiters: Deque[Hashable] = field(default_factory=deque)
waiter_set: set[Hashable] = field(default_factory=set)
last_now_ms: Optional[int] = None
last_seen_ms: int = 0

class SlidingWindowRateLimiter:
"""Thread-safe, single-process, exact sliding-window rate limiter.

Sliding-window algorithm:
  Every admitted request timestamp is stored in a deque. Before evaluating a
  request, timestamps t satisfying t <= now - window are removed. Thus a
  request exactly one full window old no longer consumes capacity. This is a
  true log-based sliding window, so it has no calendar-bucket boundary burst.

  The result is exact, with O(1) amortized deque operations and memory
  proportional to admitted requests still inside configured windows. This is
  more accurate than weighted buckets but uses more memory. Limits provide a
  hard bound on live timestamp entries, and stale client objects are evicted.

Fair sharing:
  Uncontended tenant capacity is work-conserving and can be used by any
  client. Once the tenant ceiling is reached, denied clients enter a FIFO
  queue with at most one entry per client. As capacity expires, the head gets
  the next admission, then leaves the queue. A continuously busy client must
  rejoin behind other contenders, implementing round-robin redistribution
  without permanently reserving unused per-client shares. Inactive queue
  entries expire after active_timeout_ms so an abandoned turn cannot block
  the tenant forever.

Clock behavior:
  now_ms should come from a monotonic clock. If a caller supplies a duplicate
  or decreasing timestamp, time is clamped to the tenant's greatest observed
  timestamp. This prevents expired history from becoming live again.

Concurrency and deployment assumptions:
  A single re-entrant lock makes allow, configuration, and sweeping atomic
  across threads and async tasks sharing this object. For distributed use,
  the same state transition can be implemented atomically in Redis with a
  Lua script and sorted sets, or in a transactional strongly consistent
  store. FIFO waiter metadata must be updated in the same transaction as the
  request logs; local locks alone do not coordinate multiple processes.
"""

def __init__(self, default_config: Optional[TenantConfig] = None) -> None:
    if default_config is not None:
        default_config.validate()
    self._default_config = default_config
    self._configs: Dict[Hashable, TenantConfig] = {}
    self._tenants: Dict[Hashable, _TenantState] = {}
    self._lock = threading.RLock()

def configure_tenant(self, tenant_id: Hashable, config: TenantConfig) -> None:
    """Create or replace a tenant's configuration atomically."""
    config.validate()
    with self._lock:
        self._configs[tenant_id] = config

def allow(self, tenant_id: Hashable, client_id: Hashable, now_ms: int) -> AllowResult:
    """Atomically decide whether to admit one request.

    retry_after_ms is exact for a direct client/window or tenant/window
    denial. For a FIFO fairness denial it is the earliest temporal retry;
    actual admission also depends on earlier active waiters taking or
    abandoning their turns.
    """
    if not isinstance(now_ms, int):
        raise TypeError("now_ms must be an integer")

    with self._lock:
        cfg = self._config_for(tenant_id)
        tenant = self._tenants.get(tenant_id)
        if tenant is None:
            tenant = _TenantState(last_now_ms=now_ms, last_seen_ms=now_ms)
            self._tenants[tenant_id] = tenant

        now = self._normalize_time(tenant, now_ms)
        tenant.last_seen_ms = now
        self._prune(tenant.admitted, now, cfg.tenant_window_ms)

        client = tenant.clients.get(client_id)
        if client is None:
            client = _ClientState(last_seen_ms=now)
            tenant.clients[client_id] = client
        client.last_seen_ms = now
        self._prune(client.admitted, now, cfg.client_window_ms)

        self._purge_waiters(tenant, cfg, now)
        self._evict_clients(tenant, cfg, now, preserve=client_id)

        if len(client.admitted) >= cfg.client_limit:
            self._remove_waiter(tenant, client_id)
            retry = client.admitted[0] + cfg.client_window_ms - now
            return AllowResult(False, max(0, retry))

        if len(tenant.admitted) >= cfg.tenant_limit:
            self._enqueue_waiter(tenant, client_id)
            retry = tenant.admitted[0] + cfg.tenant_window_ms - now
            return AllowResult(False, max(0, retry))

        # Capacity exists. During contention, only the FIFO head may take it.
        if tenant.waiters and tenant.waiters[0] != client_id:
            self._enqueue_waiter(tenant, client_id)
            return AllowResult(False, 0)

        if tenant.waiters and tenant.waiters[0] == client_id:
            tenant.waiters.popleft()
            tenant.waiter_set.remove(client_id)

        client.admitted.append(now)
        tenant.admitted.append(now)
        return AllowResult(True, 0)

def sweep(self, now_ms: int) -> int:
    """Evict stale clients and empty tenant runtime states.

    Applications can call this periodically when traffic is low. Normal
    allow calls also clean the tenant being accessed. Returns the number of
    client and tenant runtime objects removed. Configurations are retained.
    """
    if not isinstance(now_ms, int):
        raise TypeError("now_ms must be an integer")

    removed = 0
    with self._lock:
        for tenant_id in list(self._tenants):
            cfg = self._config_for(tenant_id)
            tenant = self._tenants[tenant_id]
            now = self._normalize_time(tenant, now_ms)
            self._prune(tenant.admitted, now, cfg.tenant_window_ms)
            self._purge_waiters(tenant, cfg, now)
            before = len(tenant.clients)
            self._evict_clients(tenant, cfg, now, preserve=None)
            removed += before - len(tenant.clients)

            if (not tenant.clients and not tenant.admitted and not tenant.waiters
                    and now - tenant.last_seen_ms >= cfg.eviction_ms):
                del self._tenants[tenant_id]
                removed += 1
    return removed

def debug_state(self, tenant_id: Hashable) -> dict[str, int]:
    """Return aggregate state sizes, useful for monitoring and tests."""
    with self._lock:
        tenant = self._tenants.get(tenant_id)
        if tenant is None:
            return {"clients": 0, "tenant_events": 0, "waiters": 0,
                    "client_events": 0}
        return {
            "clients": len(tenant.clients),
            "tenant_events": len(tenant.admitted),
            "waiters": len(tenant.waiters),
            "client_events": sum(len(c.admitted) for c in tenant.clients.values()),
        }

def _config_for(self, tenant_id: Hashable) -> TenantConfig:
    cfg = self._configs.get(tenant_id, self._default_config)
    if cfg is None:
        raise KeyError(f"tenant {tenant_id!r} has no configuration")
    return cfg

@staticmethod
def _normalize_time(tenant: _TenantState, supplied: int) -> int:
    if tenant.last_now_ms is None or supplied >= tenant.last_now_ms:
        tenant.last_now_ms = supplied
        return supplied
    return tenant.last_now_ms

@staticmethod
def _prune(events: Deque[int], now: int, window_ms: int) -> None:
    boundary = now - window_ms
    while events and events[0] <= boundary:
        events.popleft()

@staticmethod
def _enqueue_waiter(tenant: _TenantState, client_id: Hashable) -> None:
    if client_id not in tenant.waiter_set:
        tenant.waiters.append(client_id)
        tenant.waiter_set.add(client_id)

@staticmethod
def _remove_waiter(tenant: _TenantState, client_id: Hashable) -> None:
    if client_id not in tenant.waiter_set:
        return
    tenant.waiters = deque(x for x in tenant.waiters if x != client_id)
    tenant.waiter_set.remove(client_id)

def _purge_waiters(self, tenant: _TenantState, cfg: TenantConfig, now: int) -> None:
    kept: Deque[Hashable] = deque()
    kept_set: set[Hashable] = set()
    for client_id in tenant.waiters:
        client = tenant.clients.get(client_id)
        if client is None or now - client.last_seen_ms >= cfg.active_timeout_ms:
            continue
        self._prune(client.admitted, now, cfg.client_window_ms)
        # A client blocked by its own quota does not reserve a tenant turn.
        if len(client.admitted) >= cfg.client_limit:
            continue
        kept.append(client_id)
        kept_set.add(client_id)
    tenant.waiters = kept
    tenant.waiter_set = kept_set

@staticmethod
def _evict_clients(tenant: _TenantState, cfg: TenantConfig, now: int,
                   preserve: Optional[Hashable]) -> None:
    for client_id in list(tenant.clients):
        if preserve is not None and client_id == preserve:
            continue
        client = tenant.clients[client_id]
        boundary = now - cfg.client_window_ms
        while client.admitted and client.admitted[0] <= boundary:
            client.admitted.popleft()
        if (not client.admitted and client_id not in tenant.waiter_set
                and now - client.last_seen_ms >= cfg.eviction_ms):
            del tenant.clients[client_id]

class SlidingWindowRateLimiterTests(unittest.TestCase):
def make_limiter(self, **overrides: int) -> SlidingWindowRateLimiter:
values = dict(client_limit=3, client_window_ms=1000,
tenant_limit=20, tenant_window_ms=1000,
active_timeout_ms=1000, eviction_ms=2000)
values.update(overrides)
return SlidingWindowRateLimiter(TenantConfig(**values))

def test_exact_window_boundary_is_expired(self) -> None:
    limiter = self.make_limiter(client_limit=1)
    self.assertTrue(limiter.allow("t", "c", 0).allowed)
    denied = limiter.allow("t", "c", 999)
    self.assertFalse(denied.allowed)
    self.assertEqual(denied.retry_after_ms, 1)
    self.assertTrue(limiter.allow("t", "c", 1000).allowed)

def test_idle_client_returns_after_full_window(self) -> None:
    limiter = self.make_limiter(client_limit=2)
    self.assertTrue(limiter.allow("t", "c", 10).allowed)
    self.assertTrue(limiter.allow("t", "c", 11).allowed)
    self.assertFalse(limiter.allow("t", "c", 12).allowed)
    self.assertTrue(limiter.allow("t", "c", 1011).allowed)

def test_duplicate_and_backwards_clock_are_clamped(self) -> None:
    limiter = self.make_limiter(client_limit=2)
    self.assertTrue(limiter.allow("t", "c", 100).allowed)
    self.assertTrue(limiter.allow("t", "c", 100).allowed)
    denied = limiter.allow("t", "c", 50)
    self.assertFalse(denied.allowed)
    self.assertEqual(denied.retry_after_ms, 1000)
    self.assertTrue(limiter.allow("t", "c", 1100).allowed)

def test_concurrent_requests_are_atomic(self) -> None:
    limiter = self.make_limiter(client_limit=10, tenant_limit=100)
    barrier = threading.Barrier(40)
    results: list[bool] = []
    results_lock = threading.Lock()

    def worker() -> None:
        barrier.wait()
        value = limiter.allow("t", "same-client", 500).allowed
        with results_lock:
            results.append(value)

    threads = [threading.Thread(target=worker) for _ in range(40)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

    self.assertEqual(sum(results), 10)
    self.assertEqual(len(results), 40)

def test_tenant_cap_combines_all_clients(self) -> None:
    limiter = self.make_limiter(client_limit=10, tenant_limit=2)
    self.assertTrue(limiter.allow("t", "a", 0).allowed)
    self.assertTrue(limiter.allow("t", "b", 1).allowed)
    denied = limiter.allow("t", "c", 2)
    self.assertFalse(denied.allowed)
    self.assertEqual(denied.retry_after_ms, 998)

def test_saturated_tenant_uses_fifo_fair_redistribution(self) -> None:
    limiter = self.make_limiter(client_limit=10, tenant_limit=2)
    self.assertTrue(limiter.allow("t", "a", 0).allowed)
    self.assertTrue(limiter.allow("t", "a", 1).allowed)

    self.assertFalse(limiter.allow("t", "b", 2).allowed)
    self.assertFalse(limiter.allow("t", "c", 3).allowed)

    # Both old slots expire, but a cannot monopolize them: b and c queued.
    self.assertFalse(limiter.allow("t", "a", 1001).allowed)
    self.assertTrue(limiter.allow("t", "b", 1001).allowed)
    self.assertFalse(limiter.allow("t", "b", 1001).allowed)
    self.assertTrue(limiter.allow("t", "c", 1001).allowed)

def test_abandoned_fairness_turn_eventually_expires(self) -> None:
    limiter = self.make_limiter(client_limit=10, tenant_limit=1,
                                active_timeout_ms=1000, eviction_ms=2000)
    self.assertTrue(limiter.allow("t", "a", 0).allowed)
    self.assertFalse(limiter.allow("t", "gone", 1).allowed)
    # At 1001 the waiter is inactive, so another client may use capacity.
    self.assertTrue(limiter.allow("t", "b", 1001).allowed)

def test_eviction_removes_stale_but_not_active_clients(self) -> None:
    limiter = self.make_limiter(client_window_ms=100, tenant_window_ms=100,
                                active_timeout_ms=100, eviction_ms=200)
    self.assertTrue(limiter.allow("t", "stale", 0).allowed)
    self.assertTrue(limiter.allow("t", "active", 150).allowed)

    limiter.sweep(201)
    state = limiter.debug_state("t")
    self.assertEqual(state["clients"], 1)
    self.assertEqual(state["client_events"], 1)

    # The active request is not dropped before its own window expires.
    self.assertFalse(limiter.allow("t", "active", 201).allowed
                     if limiter._default_config.client_limit == 1 else False)
    self.assertEqual(limiter.debug_state("t")["clients"], 1)
    limiter.sweep(351)
    self.assertEqual(limiter.debug_state("t")["clients"], 0)

def test_configuration_is_per_tenant(self) -> None:
    limiter = SlidingWindowRateLimiter()
    limiter.configure_tenant("small", TenantConfig(1, 100, 1, 100, 100, 100))
    limiter.configure_tenant("large", TenantConfig(2, 100, 3, 100, 100, 100))
    self.assertTrue(limiter.allow("small", "c", 0).allowed)
    self.assertFalse(limiter.allow("small", "c", 1).allowed)
    self.assertTrue(limiter.allow("large", "c", 0).allowed)
    self.assertTrue(limiter.allow("large", "c", 1).allowed)

def test_memory_is_bounded_by_live_limits_and_sweep(self) -> None:
    limiter = self.make_limiter(client_limit=5, tenant_limit=7,
                                client_window_ms=100, tenant_window_ms=100,
                                active_timeout_ms=100, eviction_ms=100)
    for index in range(100):
        limiter.allow("t", f"c{index}", 0)
    state = limiter.debug_state("t")
    self.assertLessEqual(state["tenant_events"], 7)
    self.assertLessEqual(state["client_events"], 7)
    limiter.sweep(100)
    self.assertEqual(limiter.debug_state("t")["clients"], 0)

if name == "main":
unittest.main()

Result

#1 | Winner

Winning Votes

3 / 3

Average Score

86

Total Score

91

Overall Comments

Answer A provides a comprehensive and robust implementation of the rate limiter, fully addressing all functional requirements, including the complex fair-sharing policy for tenant saturation. The code is well-structured, uses appropriate data structures, and includes thorough documentation and a strong test suite covering all specified edge cases. Its memory management via explicit sweep and configurable eviction is well-designed.

View Score Details

Correctness

Weight 35%
90

Answer A correctly implements all aspects of the rate limiter, including the complex fair-sharing policy for tenant saturation and robust clock handling. All limits and window logic are precise.

Completeness

Weight 20%
95

Answer A is highly complete, addressing all functional requirements, including the nuanced fair-sharing and comprehensive memory eviction. It provides clear explanations, assumptions, and distributed design notes as requested.

Code Quality

Weight 20%
85

The code in Answer A is well-structured, uses `dataclasses` for clear state management, and has good inline documentation. The use of `threading.RLock` is appropriate for re-entrant operations. Methods are logically separated and clean.

Practical Value

Weight 15%
90

Answer A offers high practical value due to its complete feature set, especially the fair-sharing mechanism and controlled memory eviction via the `sweep` method. The `debug_state` method is also a useful addition for monitoring.

Instruction Following

Weight 10%
95

Answer A meticulously follows all instructions, including the complex fair-sharing policy, true sliding window, concurrency safety, memory bounding, and addressing all specified edge cases in both code and tests. All deliverables are provided.

Judge Models OpenAI GPT-5.5

Total Score

83

Overall Comments

Answer A provides a substantially complete, runnable Python implementation with exact log-based sliding windows, per-tenant configuration, tenant-wide caps, concurrency protection, stale-state cleanup, retry calculations, explicit clock-regression handling, and a broad unittest suite. Its FIFO waiter mechanism is a credible fair-sharing policy under tenant saturation, though retry_after_ms for fairness queue denials can be imprecise and the waiter/client state for many denied unique clients could still grow until timeout or sweeping. Overall it closely matches the requested library behavior and edge cases.

View Score Details

Correctness

Weight 35%
83

Uses exact timestamp logs with correct boundary pruning, enforces both per-client and tenant caps, clamps backward time, and serializes mutations safely. The FIFO fairness logic is mostly correct, but retry_after_ms for non-head fairness denials can be 0 and memory for many waiting denied clients is only bounded over time rather than immediately.

Completeness

Weight 20%
86

Covers the requested API, per-tenant adjustable configuration, exact sliding windows, tenant cap, fairness queue, concurrency safety, eviction/sweep, assumptions, distributed extension notes, and tests for nearly all specified edge cases. Minor gaps include imperfect fairness retry reporting and somewhat awkward eviction test code.

Code Quality

Weight 20%
80

Well-structured dataclasses, clear public methods, validation, inline documentation, type hints, and encapsulated helper methods. The single global lock is simple and safe but limits scalability, and a few internals/tests are somewhat clunky.

Practical Value

Weight 15%
81

Practical as a reusable single-process library with monitoring/debug state, periodic sweeping, configurable tenants, and a credible path to Redis/Lua for distributed use. The global lock and timestamp-log memory cost may limit high-scale deployments, but the tradeoffs are stated.

Instruction Following

Weight 10%
90

Follows the requested deliverables closely: complete code, documented algorithm and tradeoffs, explicit assumptions, distributed extension, and tests for the named edge cases. The API returns an AllowResult instead of a tuple, which is acceptable under the prompt.

Total Score

85

Overall Comments

Answer A is a near-complete, production-quality solution. It implements an exact log-based sliding window with correct boundary semantics, a genuine fair-redistribution mechanism via a FIFO waiter queue with activity timeouts, tenant-clamped handling of backwards and duplicate clocks, a single RLock for atomicity, and bounded memory through inline eviction plus an explicit sweep API. All six mandated edge cases have targeted tests, and the docstring explains accuracy/memory tradeoffs, assumptions, and a concrete distributed extension. Weaknesses are minor: one test assertion is vacuous due to a confusing ternary, retry_after_ms of 0 on a fairness denial is ambiguous, and per-call eviction scans add overhead for tenants with many clients.

View Score Details

Correctness

Weight 35%
86

The log-based sliding window is exact: pruning uses t <= now - window so a request exactly one full window old expires, and the boundary test verifies retry_after_ms of 1 at t=999 and admission at t=1000. Backwards/duplicate clocks are clamped to the tenant's greatest observed timestamp, preventing expired history from reviving. The FIFO waiter queue provably enforces round-robin redistribution under tenant saturation (verified by tracing the saturation test: client a is blocked while queued clients b and c each get a freed slot). The single RLock makes allow atomic, and the 40-thread test admits exactly 10. Minor flaws: retry_after_ms of 0 for a fairness denial is ambiguous, and one eviction test contains a vacuous conditional assertion that never actually exercises the allow call.

Completeness

Weight 20%
87

Every functional requirement is addressed: per-tenant configuration, exact sliding window, tenant global cap with genuine FIFO fair redistribution, thread safety, eviction via both inline cleanup and an explicit sweep API, plus a debug_state helper. All six mandated edge cases have dedicated tests (exact boundary, idle return, concurrent race, clamped backwards/duplicate clock, saturation with fair redistribution, stale eviction preserving active clients), and it additionally covers abandoned waiter turns and memory bounds. Assumptions and a Redis/Lua distributed extension are documented in the docstring.

Code Quality

Weight 20%
82

Well-structured with frozen dataclasses for config and results, validated configuration, typed signatures, clear separation of pruning, waiter purging, and eviction helpers, and an unusually thorough class docstring covering algorithm, fairness semantics, clock behavior, and distributed extension. Weaknesses: the eviction test contains a confusing vacuous ternary assertion, per-call full-client eviction adds O(clients) overhead to every allow, and _remove_waiter rebuilds the deque in O(n).

Practical Value

Weight 15%
81

Deployable as-is for a single process: deterministic injected timestamps make it testable, debug_state supports monitoring, sweep gives operators explicit memory control, waiter timeouts prevent abandoned clients from starving a tenant, and the distributed migration path (Redis sorted sets plus Lua, with waiter metadata in the same transaction) is concretely described. The per-allow eviction scan is the main performance caveat for tenants with many clients.

Instruction Following

Weight 10%
86

Follows the specification closely: allow(tenant_id, client_id, now_ms) signature, retry_after_ms in the result, per-tenant adjustable config, true log-based sliding window with explicit accuracy/memory tradeoff discussion, fair redistribution under saturation, concurrency safety, bounded memory, all six edge cases in tests, and explicit assumptions plus a distributed extension note. Essentially every deliverable and edge case listed in the prompt is satisfied.

import time
import threading
from collections import deque
import unittest
from typing import Dict, Tuple, Optional

Helper data class for tenant configuration

class TenantConfig:
def init(self, client_rate: int, client_window_ms: int, tenant_rate: int, tenant_window_ms: int):
self.client_rate = client_rate
self.client_window_ms = client_window_ms
self.tenant_rate = tenant_rate
self.tenant_window_ms = tenant_window_ms

class SlidingWindowRateLimiter:
"""
A thread-safe, multi-tenant rate limiter using a sliding window log algorithm.

This implementation provide...
Show Full Answer

import time
import threading
from collections import deque
import unittest
from typing import Dict, Tuple, Optional

Helper data class for tenant configuration

class TenantConfig:
def init(self, client_rate: int, client_window_ms: int, tenant_rate: int, tenant_window_ms: int):
self.client_rate = client_rate
self.client_window_ms = client_window_ms
self.tenant_rate = tenant_rate
self.tenant_window_ms = tenant_window_ms

class SlidingWindowRateLimiter:
"""
A thread-safe, multi-tenant rate limiter using a sliding window log algorithm.

This implementation provides per-client rate limiting under a global per-tenant cap.
It uses a fair-sharing policy where no single client can exhaust the tenant's quota
due to its own individual limit.

Algorithm: Sliding Window Log
- For each client and tenant, we maintain a deque (a list-like container with fast appends
  and pops from both ends) of timestamps of requests received within the window.
- When a new request arrives, we first discard all timestamps older than the current
  time minus the window size.
- Then, we check if the number of remaining timestamps is less than the configured rate limit.
- If the request is allowed, the current timestamp is added to the log.

Tradeoffs:
- Accuracy: This algorithm is perfectly accurate. It correctly tracks the request count
  within the precise sliding window, avoiding issues with fixed-window counters where
  bursts at the boundary of windows can exceed the rate.
- Memory Usage: The memory footprint is proportional to the rate limit, as it stores one
  timestamp per allowed request within the window. For a client with a rate of 1000, it
  will store up to 1000 timestamps (approx. 8KB per client). This can be significant for
  systems with a very large number of active clients or very high rate limits.

Assumptions:
- Single-Process Deployment: State is stored in memory within the class instance. It is
  thread-safe for use within a single application process.
- Monotonically Increasing Clock: The `now_ms` parameter is expected to be a monotonically
  increasing timestamp in milliseconds. The limiter protects itself against time going
  backwards by ignoring requests with timestamps older than the most recently processed one.

Distributed Extension:
To support a distributed environment (e.g., multiple API servers), the state must be
externalized to a central, low-latency store like Redis.
- State Storage: A Redis Sorted Set could be used for each client/tenant log, with both the
  score and value being the request timestamp. This allows for efficient range queries.
- Atomicity: Redis transactions (MULTI/EXEC) or Lua scripts would be essential to perform
  the check-and-update operation (prune old timestamps, check count, add new timestamp)
  atomically, preventing race conditions between different servers.
- Eviction: Redis's built-in key expiration (`EXPIRE`) would naturally handle the eviction
  of stale client data.
"""

def __init__(self):
    self._tenants: Dict[str, Dict] = {}
    self._lock = threading.Lock()  # Global lock for adding/removing tenants

def add_tenant(self, tenant_id: str, config: TenantConfig):
    """Adds or updates a tenant's configuration."""
    with self._lock:
        if tenant_id not in self._tenants:
            self._tenants[tenant_id] = {
                "config": config,
                "clients": {},
                "tenant_log": deque(),
                "lock": threading.Lock() # Per-tenant lock for thread safety
            }
        else:
            # Update config on the fly
            self._tenants[tenant_id]['config'] = config

def allow(self, tenant_id: str, client_id: str, now_ms: Optional[int] = None) -> Tuple[bool, int]:
    """
    Determines if a request is allowed for a given tenant and client.

    Args:
        tenant_id: The identifier for the tenant.
        client_id: The identifier for the client.
        now_ms: The current timestamp in milliseconds. If None, the system time is used.

    Returns:
        A tuple (is_allowed, retry_after_ms).
        - is_allowed (bool): True if the request is permitted, False otherwise.
        - retry_after_ms (int): If denied, the number of milliseconds to wait before the
          next request might be allowed. 0 if the request is allowed.
    """
    if now_ms is None:
        now_ms = int(time.time() * 1000)

    with self._lock:
        if tenant_id not in self._tenants:
            raise ValueError(f"Tenant '{tenant_id}' not configured.")
        tenant_state = self._tenants[tenant_id]

    with tenant_state['lock']:
        config = tenant_state['config']

        # 1. Get or create client state
        if client_id not in tenant_state['clients']:
            tenant_state['clients'][client_id] = {"log": deque(), "last_seen_ms": 0}
        client_state = tenant_state['clients'][client_id]

        # Defend against clock going backwards
        if now_ms < client_state['last_seen_ms']:
            return (False, 1) # Deny and suggest a small retry delay

        # 2. Prune and check per-client limit
        client_allowed, client_retry_ms = self._check_limit(
            client_state['log'], config.client_rate, config.client_window_ms, now_ms
        )
        if not client_allowed:
            return (False, client_retry_ms)

        # 3. Prune and check per-tenant limit
        tenant_allowed, tenant_retry_ms = self._check_limit(
            tenant_state['tenant_log'], config.tenant_rate, config.tenant_window_ms, now_ms
        )
        if not tenant_allowed:
            return (False, tenant_retry_ms)

        # 4. If both are allowed, record the request and permit it
        client_state['log'].append(now_ms)
        client_state['last_seen_ms'] = now_ms
        tenant_state['tenant_log'].append(now_ms)

        # 5. Opportunistic cleanup of stale clients for this tenant
        self._cleanup_stale_clients(tenant_state, now_ms)

        return (True, 0)

def _check_limit(self, log: deque, rate: int, window_ms: int, now_ms: int) -> Tuple[bool, int]:
    """Helper to prune a timestamp log and check it against a rate limit."""
    # Prune old timestamps
    window_start_ms = now_ms - window_ms
    while log and log[0] <= window_start_ms:
        log.popleft()

    # Check if limit is exceeded
    if len(log) >= rate:
        oldest_timestamp = log[0]
        retry_after_ms = (oldest_timestamp + window_ms) - now_ms
        return (False, max(1, retry_after_ms)) # Ensure retry is at least 1ms

    return (True, 0)

def _cleanup_stale_clients(self, tenant_state: Dict, now_ms: int):
    """
    Removes clients that have been inactive for a long time.
    An inactive client is one whose last request is older than twice the window size.
    This is an opportunistic cleanup, run inline with a request.
    """
    config = tenant_state['config']
    stale_threshold_ms = config.client_window_ms * 2
    stale_clients = [
        cid for cid, cstate in tenant_state['clients'].items()
        if now_ms - cstate['last_seen_ms'] > stale_threshold_ms
    ]
    for client_id in stale_clients:
        del tenant_state['clients'][client_id]

--- Test Suite ---

class TestSlidingWindowRateLimiter(unittest.TestCase):

def setUp(self):
    self.limiter = SlidingWindowRateLimiter()
    self.tenant_config = TenantConfig(
        client_rate=5,
        client_window_ms=1000,
        tenant_rate=15,
        tenant_window_ms=1000
    )
    self.limiter.add_tenant('test_tenant', self.tenant_config)
    self.now = int(time.time() * 1000)

def test_basic_client_limit(self):
    """A single client should be throttled after exceeding its rate."""
    for i in range(5):
        allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + i)
        self.assertTrue(allowed)

    allowed, retry_after = self.limiter.allow('test_tenant', 'client1', self.now + 5)
    self.assertFalse(allowed)
    self.assertGreater(retry_after, 990) # Should be around 1000ms - 5ms

def test_window_sliding(self):
    """Requests should be allowed again after the window has passed."""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)

    # This request should be denied
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 6)
    self.assertFalse(allowed)

    # After the window for the first request passes, a new one is allowed
    # First request was at self.now, window is 1000ms.
    # At self.now + 1001, the first request expires.
    allowed, _ = self.limiter.allow('test_tenant', 'client1', self.now + 1001)
    self.assertTrue(allowed, "Request should be allowed after window slides")

def test_idle_client_returns(self):
    """An idle client's quota should be fresh when it returns."""
    for i in range(5):
        self.limiter.allow('test_tenant', 'client1', self.now + i)
    
    # Client goes idle for more than a window
    new_time = self.now + 2000
    for i in range(5):
        allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + i)
        self.assertTrue(allowed, "Requests should be allowed for a returning client")
    
    allowed, _ = self.limiter.allow('test_tenant', 'client1', new_time + 5)
    self.assertFalse(allowed, "Client should be throttled again after using new quota")

def test_tenant_limit_and_fairness(self):
    """Multiple clients should collectively hit the tenant limit, demonstrating fairness."""
    # 3 clients, each makes 5 requests. Total 15 requests, which is the tenant limit.
    for i in range(5):
        # Client 1
        self.assertTrue(self.limiter.allow('test_tenant', 'c1', self.now + i)[0])
        # Client 2
        self.assertTrue(self.limiter.allow('test_tenant', 'c2', self.now + i)[0])
        # Client 3
        self.assertTrue(self.limiter.allow('test_tenant', 'c3', self.now + i)[0])

    # At this point, tenant log has 15 requests. The next request from any client should fail.
    self.assertFalse(self.limiter.allow('test_tenant', 'c4', self.now + 6)[0])
    self.assertFalse(self.limiter.allow('test_tenant', 'c1', self.now + 6)[0])

    # Test fairness: one client hits its limit, but another can proceed.
    limiter = SlidingWindowRateLimiter()
    limiter.add_tenant('fair_tenant', TenantConfig(client_rate=2, client_window_ms=1000, tenant_rate=10, tenant_window_ms=1000))
    # Hog client uses up its quota
    self.assertTrue(limiter.allow('fair_tenant', 'hog', self.now)[0])
    self.assertTrue(limiter.allow('fair_tenant', 'hog', self.now + 1)[0])
    self.assertFalse(limiter.allow('fair_tenant', 'hog', self.now + 2)[0]) # Hog is blocked

    # Another client should still be able to make requests
    self.assertTrue(limiter.allow('fair_tenant', 'fair_client', self.now + 3)[0])

def test_stale_client_eviction(self):
    """Stale clients should be removed from memory."""
    self.limiter.allow('test_tenant', 'stale_client', self.now)
    self.assertIn('stale_client', self.limiter._tenants['test_tenant']['clients'])

    # Make a new request far in the future, triggering cleanup
    future_time = self.now + self.tenant_config.client_window_ms * 3
    self.limiter.allow('test_tenant', 'active_client', future_time)

    self.assertNotIn('stale_client', self.limiter._tenants['test_tenant']['clients'])

def test_concurrency(self):
    """Should handle concurrent requests to the same client correctly."""
    limiter = SlidingWindowRateLimiter()
    limiter.add_tenant('concurrent_tenant', TenantConfig(client_rate=100, client_window_ms=1000, tenant_rate=1000, tenant_window_ms=1000))
    start_time = int(time.time() * 1000)
    num_threads = 10
    reqs_per_thread = 15
    allowed_count = [0]
    lock = threading.Lock()

    def worker():
        for _ in range(reqs_per_thread):
            now = int(time.time() * 1000)
            allowed, _ = limiter.allow('concurrent_tenant', 'shared_client', now)
            if allowed:
                with lock:
                    allowed_count[0] += 1
            time.sleep(0.001) # small sleep to increase chance of contention

    threads = [threading.Thread(target=worker) for _ in range(num_threads)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    # The number of allowed requests should be exactly the client rate limit.
    self.assertEqual(allowed_count[0], 100)

def test_clock_regression(self):
    """Should deny requests if the clock moves backwards."""
    self.assertTrue(self.limiter.allow('test_tenant', 'client1', self.now + 100)[0])
    # A request with an earlier timestamp should be denied
    allowed, retry_after = self.limiter.allow('test_tenant', 'client1', self.now + 99)
    self.assertFalse(allowed)
    self.assertEqual(retry_after, 1)

if name == 'main':
unittest.main()

Result

#2

Winning Votes

0 / 3

Average Score

50

Total Score

46

Overall Comments

Answer B implements a basic sliding-window rate limiter with per-client and per-tenant limits. However, it critically fails to implement the fair-sharing policy for tenant saturation, which was a core requirement. Its memory eviction is opportunistic and less controlled, and its test suite, while present, does not adequately cover the fair-sharing aspect it claims to address. The code quality is acceptable but less refined than Answer A.

View Score Details

Correctness

Weight 35%
40

Answer B fails to correctly implement the fair-sharing policy for tenant saturation, which is a major functional requirement. While basic client and tenant limits work, the core fairness mechanism is absent. The clock regression retry_after_ms is a minimal '1' which is not very informative.

Completeness

Weight 20%
40

Answer B is incomplete due to the missing fair-sharing policy. Its memory eviction is only opportunistic and lacks a dedicated sweep mechanism, making it less robust than required. The `now_ms` parameter having a default value is a minor deviation from the prompt.

Code Quality

Weight 20%
65

Answer B's code quality is acceptable. It uses dictionaries for state, which is less structured than dataclasses. The `allow` method is somewhat long, and the opportunistic cleanup is less explicit. The use of per-tenant locks is a good pattern, but the overall solution is less refined.

Practical Value

Weight 15%
50

Answer B has limited practical value for multi-tenant scenarios because it lacks the crucial fair-sharing policy. The opportunistic cleanup might not be sufficient for all production use cases, and there's no explicit way to trigger a full cleanup.

Instruction Following

Weight 10%
30

Answer B fails to follow a critical instruction: implementing the fair-sharing policy for tenant saturation. Its eviction mechanism is less robust than specified, and the test for 'tenant_limit_and_fairness' does not actually test fair redistribution among competing clients when the tenant is saturated.

Judge Models OpenAI GPT-5.5

Total Score

48

Overall Comments

Answer B implements a basic thread-safe sliding-window log limiter with per-client and per-tenant caps, documentation, and some tests. However, it does not implement real fair redistribution when a tenant is saturated, has weak and partly incorrect clock-regression handling across tenants/clients, cleanup is opportunistic and incomplete, and the tests do not cover several required edge cases rigorously. It is a reasonable simple limiter but incomplete for the benchmark task.

View Score Details

Correctness

Weight 35%
45

Correct for simple per-client and tenant sliding-window checks in normal monotonic use, but it lacks true tenant-saturation fair sharing. Clock handling is only per client and can leave the tenant log out of timestamp order across clients, breaking pruning/retry assumptions. It also denies backward timestamps rather than normalizing them and has weaker cleanup semantics.

Completeness

Weight 20%
42

Includes an API, configuration, sliding logs, tenant cap, explanation, and some tests, but misses the central fair redistribution requirement. Tests do not explicitly check exact boundary behavior, duplicate timestamps, real tenant saturation fairness, or safe eviction of active clients in depth.

Code Quality

Weight 20%
58

Readable and compact, with straightforward helper methods and comments. However, it relies on loosely typed dictionaries for state, has less robust synchronization around configuration updates, weaker validation, and simpler cleanup/fairness design than the task requires.

Practical Value

Weight 15%
50

Useful for a small single-process service needing basic rate limiting, and the Redis extension discussion is reasonable. Its lack of saturated-tenant fairness and fragile clock handling make it less suitable for the multi-tenant quota scenario described.

Instruction Following

Weight 10%
50

Chooses an allowed language and provides code, comments, and tests, but does not fulfill the important fair-sharing requirement and only partially addresses several required edge cases. The claimed fairness policy in the prose is not actually implemented.

Total Score

55

Overall Comments

Answer B delivers a clean, readable sliding-window-log limiter with per-client and per-tenant caps, two-level locking, opportunistic eviction, and good documentation of tradeoffs, assumptions, and a Redis-based distributed extension. However, it omits the fair-redistribution requirement entirely — under tenant saturation any client can monopolize freed capacity — and its 'fairness' test only demonstrates independent per-client limits, which the judging policy explicitly calls incomplete. Additional issues include per-client-only backwards-clock defense that can leave the shared tenant deque unordered, cleanup that runs only on allowed requests (risking memory growth under sustained saturation), no explicit exact-boundary test, and a concurrency test tied to real wall-clock time.

View Score Details

Correctness

Weight 35%
53

The per-client and per-tenant sliding-window logs are correct in isolation, and boundary pruning uses <= consistently. However, requirement 4's fairness is not actually implemented: when the tenant is saturated, any client is denied identically and the first client to retry after expiry can monopolize freed capacity; there is no redistribution mechanism. The backwards-clock defense is per-client only, so out-of-order timestamps across different clients can insert unordered entries into the shared tenant deque, breaking the prune loop's ordering assumption. Stale-client cleanup runs only on the successful-allow path, so under sustained tenant saturation, denied clients accumulate state indefinitely, violating the bounded-memory requirement in that scenario.

Completeness

Weight 20%
50

Covers per-client windows, tenant cap, threading, eviction, algorithm tradeoffs, assumptions, and a distributed extension sketch. But the central differentiator — fair redistribution when the tenant is saturated — is absent, and the 'fairness' test only shows that one client's own per-client limit does not block another client, which is just independent limiting. There is no explicit exact-boundary test (window sliding is tested at +1001, not at the boundary itself), and duplicate-timestamp handling is only implicit. Per the judging policy, solutions with only independent per-client limits are incomplete.

Code Quality

Weight 20%
64

Readable code with a helpful class docstring explaining the algorithm, tradeoffs, assumptions, and Redis extension, plus a sensible _check_limit helper. However, state is modeled as nested untyped dicts of dicts rather than dataclasses, tests directly poke private attributes (_tenants), the concurrency test depends on real wall-clock time and sleeps (potentially flaky), and there is no config validation. The two-level locking scheme is reasonable but the config/tuple-based API is less polished.

Practical Value

Weight 15%
56

Usable for basic per-client plus tenant-cap limiting and the Redis extension notes are practical. But in production the missing fair-sharing means one aggressive client can seize all freed tenant capacity, the clock-regression response (blanket denial with retry 1) can reject legitimate traffic under minor timestamp skew, and cleanup skipping on denied requests risks memory growth exactly when a tenant is under attack — the scenario a rate limiter most needs to survive.

Instruction Following

Weight 10%
54

Matches the API shape, delivers a genuine sliding-window log, states assumptions, discusses tradeoffs, and sketches the distributed design as requested. But it fails requirement 4's core mandate that remaining tenant capacity be shared fairly among active clients when saturated, and the corresponding edge case (tenant saturation and fair redistribution) is neither implemented nor genuinely tested, which is an explicit instruction miss.

Comparison Summary

Final rank order is determined by judge-wise rank aggregation (average rank + Borda tie-break). Average score is shown for reference.

Judges: 3

Winning Votes

3 / 3

Average Score

86
View this answer

Winning Votes

0 / 3

Average Score

50
View this answer

Judging Results

Why This Side Won

Answer A wins decisively on the weighted result. It leads on correctness (35%) with an exact boundary-tested sliding window, clamped clock handling, and a verified atomic concurrency model, while B has a tenant-log ordering flaw and saturation-dependent memory growth. A also leads on completeness (20%) and instruction following (10%) because it implements and tests the fair-redistribution requirement that the judging policy names as an important differentiator, whereas B only implements independent per-client limits, which the policy explicitly labels incomplete. A further leads on code quality (20%) and practical value (15%). Since A scores higher on every criterion, the weighted total unambiguously favors A.

Judge Models OpenAI GPT-5.5

Why This Side Won

Answer A wins because it satisfies the heavily weighted correctness requirements far better: it uses a true sliding-window log for both client and tenant limits, computes sensible retry times, handles boundary expiration, clamps backward time, protects state with a lock, evicts stale state, and implements an explicit FIFO fair-sharing mechanism for saturated tenants. Answer B lacks the key fair-sharing behavior, has problematic cross-client timestamp ordering under clock regression, and only partially covers the required edge cases.

Why This Side Won

Answer A is the clear winner because it fully and correctly implements all functional requirements, most notably the challenging fair-sharing policy for tenant saturation, which Answer B completely misses. Answer A also demonstrates superior code quality, more robust memory management, and a more comprehensive test suite that genuinely covers all specified edge cases. The missing fair-sharing in Answer B is a critical functional failure that significantly reduces its correctness and completeness.

X f L