Rate Limiter with Tiers, Runtime Config Changes and Multiple Rules

Quick Overview

Implement a rate limiter using a token bucket or sliding window, then support client tiers whose limits change while the service runs, several rules checked on one request, and deterministic tests with a mock clock. It tests algorithm choice, safe configuration swaps and thread-safe all-or-nothing checks.

Rate Limiter with Tiers, Runtime Config Changes and Multiple Rules

Company: Snowflake

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Technical Screen

Implement a rate limiter that a service calls before handling each request: `allow(client_id)` returns whether the request may proceed. Build it with a token bucket or a sliding window, then extend it in the directions the interview took: clients belong to tiers with different limits, an operator can change a tier's limits while the service is running, one request can be subject to several rules at once, and the whole thing must be testable with a mock clock. ```python limiter = RateLimiter(config, clock=fake_clock) limiter.allow("client-42") # True or False ``` ### Clarifying Questions - Does the limiter run inside one process, or must several servers share the same limits? - Is a rejected request dropped, or should the caller learn how long to wait? - Should short bursts above the average rate be allowed, and how large? - When a tier's limits change, should a client's current state carry over or reset? - Does a request rejected by one rule still count against the other rules? ### Part 1 — One limit Implement `allow(client_id)` for a single limit, for example at most N requests per period, per client. Choose token bucket or sliding window and justify it. ```hint Compute instead of tick Think about how the limiter can bring a client's state up to date at the moment a request arrives, so that no background timer is needed. ``` #### What This Part Should Cover - The per-client state and the update performed on each call - Burst behavior and accuracy at window boundaries for the chosen algorithm - Time and memory per client, and thread safety ### Part 2 — Tiers that change at runtime Each client belongs to a tier, and each tier has its own limits. An operator can change a tier's limits, or move a client to another tier, without restarting the service. ```hint Separate the rule from the state Decide what each client's stored state should depend on, so that a new rule can be applied to it without corrupting it. ``` #### What This Part Should Cover - How configuration is looked up on each call and swapped safely - What happens to a client's existing state when its limits change - How a bad configuration is rejected before it takes effect ### Part 3 — Several rules per request A request can be subject to several rules at once, for example a per-second limit and a per-day limit, or a per-client limit and a global limit. It is allowed only if every rule allows it. ```hint All or nothing Consider what happens to the first rule's budget when the second rule rejects the request. ``` #### What This Part Should Cover - How rules are evaluated so that a rejected request consumes nothing - Thread safety when several rules are checked together - What the caller learns about which rule rejected it ### Part 4 — Testing with a mock clock Show how the limiter is tested without real waiting. ```hint Time as a dependency Ask where the limiter reads the current time, and what a test needs to control. ``` #### What This Part Should Cover - How the clock is injected - The specific test cases: boundaries, refill, bursts, configuration changes, rule interactions - Deterministic tests for concurrent callers ### What a Strong Answer Covers - Settles single-process versus shared limits, burst size and rejection behavior first - A correct lazy-refill token bucket or sliding window, with its boundary behavior explained - Configuration kept separate from per-client state, swapped atomically, with defined carry-over - Multi-rule checks that never consume budget for a rejected request - A clock abstraction and a concrete list of deterministic tests ### Follow-up Questions - Several servers must enforce one shared limit per client. What changes, and what happens when the shared store is slow or unavailable? - Millions of clients are idle most of the time. How do you keep memory bounded? - Some requests are more expensive than others. How do you charge them differently? - How do you return a precise "retry after" value to a rejected client?

Overview: Implement a rate limiter using a token bucket or sliding window, then support client tiers whose limits change while the service runs, several rules checked on one request, and deterministic tests with a mock clock. It tests algorithm choice, safe configuration swaps and thread-safe all-or-nothing checks.

Snowflake logo
Snowflake
Apr 28, 2026
mediumSoftware EngineerTechnical ScreenSoftware Engineering Fundamentals
1
0

Implement a rate limiter that a service calls before handling each request: allow(client_id) returns whether the request may proceed. Build it with a token bucket or a sliding window, then extend it in the directions the interview took: clients belong to tiers with different limits, an operator can change a tier's limits while the service is running, one request can be subject to several rules at once, and the whole thing must be testable with a mock clock.

limiter = RateLimiter(config, clock=fake_clock)
limiter.allow("client-42")   # True or False

Clarifying Questions Guidance

  • Does the limiter run inside one process, or must several servers share the same limits?
  • Is a rejected request dropped, or should the caller learn how long to wait?
  • Should short bursts above the average rate be allowed, and how large?
  • When a tier's limits change, should a client's current state carry over or reset?
  • Does a request rejected by one rule still count against the other rules?

Part 1 — One limit

Implement allow(client_id) for a single limit, for example at most N requests per period, per client. Choose token bucket or sliding window and justify it.

What This Part Should Cover Guidance

  • The per-client state and the update performed on each call
  • Burst behavior and accuracy at window boundaries for the chosen algorithm
  • Time and memory per client, and thread safety

Part 2 — Tiers that change at runtime

Each client belongs to a tier, and each tier has its own limits. An operator can change a tier's limits, or move a client to another tier, without restarting the service.

What This Part Should Cover Guidance

  • How configuration is looked up on each call and swapped safely
  • What happens to a client's existing state when its limits change
  • How a bad configuration is rejected before it takes effect

Part 3 — Several rules per request

A request can be subject to several rules at once, for example a per-second limit and a per-day limit, or a per-client limit and a global limit. It is allowed only if every rule allows it.

What This Part Should Cover Guidance

  • How rules are evaluated so that a rejected request consumes nothing
  • Thread safety when several rules are checked together
  • What the caller learns about which rule rejected it

Part 4 — Testing with a mock clock

Show how the limiter is tested without real waiting.

What This Part Should Cover Guidance

  • How the clock is injected
  • The specific test cases: boundaries, refill, bursts, configuration changes, rule interactions
  • Deterministic tests for concurrent callers

What a Strong Answer Covers Guidance

  • Settles single-process versus shared limits, burst size and rejection behavior first
  • A correct lazy-refill token bucket or sliding window, with its boundary behavior explained
  • Configuration kept separate from per-client state, swapped atomically, with defined carry-over
  • Multi-rule checks that never consume budget for a rejected request
  • A clock abstraction and a concrete list of deterministic tests

Follow-up Questions Guidance

  • Several servers must enforce one shared limit per client. What changes, and what happens when the shared store is slow or unavailable?
  • Millions of clients are idle most of the time. How do you keep memory bounded?
  • Some requests are more expensive than others. How do you charge them differently?
  • How do you return a precise "retry after" value to a rejected client?