Overview
A rate limiter component decides whether a request from a client is allowed right now, based on a policy such as 100 requests per minute. The common algorithms are token bucket (tokens refill at a steady rate, allowing bursts up to capacity), leaky bucket (constant outflow), fixed window counters, and sliding window log or counter.
An LLD answer should define a clean interface (tryAcquire(clientId)), make algorithms pluggable (Strategy), support per-client and per-endpoint rules, use an injectable clock for tests, and be thread-safe. For distributed use, the state moves to Redis with atomic scripts, but the in-process design is the same.
You receive one token per second, up to 10 in your pocket. Each game costs a token. You can play 10 quick games in a burst, but then only as fast as tokens arrive.
When to use it
- Interview prompt: 'Design a rate limiter class'.
- Protecting APIs, logins, and expensive operations.
- Client-side throttling of calls to third-party APIs.
Where it shows up in interviews
Recognize it when: limit N actions per time window per key.
- Design a rate limiter
- Logger rate limiter (LeetCode 359)
- Design hit counter (LeetCode 362)
Where it is used in real software
Token bucket implementations for Java services.
Leaky bucket style limiting at the proxy.
GitHub, Stripe, and OpenAI enforce per-key limits and return 429 with retry headers.
Key terms
- Token bucket
- Capacity plus refill rate; allows bursts.
- Sliding window log
- Timestamps of recent requests; exact but memory heavy.
- Fixed window
- Counter per time window; bursts at edges.
- Key
- What is limited: user, API key, IP, endpoint.
- Retry-After
- When the client may try again.
How it works, step by step
- 1Define the interface
tryAcquire(key): boolean, plus retryAfter.
- 2Pick the algorithm
Token bucket is a common default.
- 3Store state per key
ConcurrentHashMap of buckets.
- 4Refill lazily on access
tokens += elapsed x rate, capped.
- 5Make it thread-safe and testable
Synchronize per bucket, inject a clock.
Token bucket trace
Capacity 3, refill 1 token/second
| Time (s) | Request | Tokens before | Allowed? | Tokens after |
|---|---|---|---|---|
| 0.0 | r1 | 3 | Yes | 2 |
| 0.1 | r2 | 2 | Yes | 1 |
| 0.2 | r3 | 1 | Yes | 0 |
| 0.3 | r4 | 0 | No (retry in 0.7 s) | 0 |
| 1.3 | r5 | 1 (refilled) | Yes | 0 |
NOWTime (s): 0.0 | Request: r1 | Tokens before: 3 | Allowed?: Yes | Tokens after: 2
Bursts up to capacity are allowed; sustained rate is bounded by the refill rate.
Implementation
import java.time.Clock;import java.util.concurrent.ConcurrentHashMap; public interface RateLimiter { boolean tryAcquire(String key); } public final class TokenBucketLimiter implements RateLimiter { private static final class Bucket { double tokens; long lastRefillNanos; Bucket(double tokens, long now) { this.tokens = tokens; this.lastRefillNanos = now; } } private final int capacity; private final double refillPerSecond; private final Clock clock; private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>(); public TokenBucketLimiter(int capacity, double refillPerSecond, Clock clock) { this.capacity = capacity; this.refillPerSecond = refillPerSecond; this.clock = clock; } @Override public boolean tryAcquire(String key) { long now = clock.millis() * 1_000_000L; Bucket b = buckets.computeIfAbsent(key, k -> new Bucket(capacity, now)); synchronized (b) { // per-key lock double elapsed = (now - b.lastRefillNanos) / 1e9; b.tokens = Math.min(capacity, b.tokens + elapsed * refillPerSecond); b.lastRefillNanos = now; if (b.tokens >= 1) { b.tokens -= 1; return true; } return false; } }}Complexity and performance
Two numbers per key.
Stores timestamps.
Trade-offs
Sliding logs are exact but store every timestamp; token buckets and sliding counters approximate with constant memory.
In-process limiters are fast but per instance; global limits need shared state (Redis) or division of quota.
Variants and related techniques
Weighted average of current and previous window counts.
Smooths output to a constant rate.
Common mistakes
- Fixed windows allowing 2x bursts at boundaries.
Fix: Use sliding windows or token buckets.
- Unbounded map of keys.
Fix: Expire idle buckets.
- Using System.currentTimeMillis directly.
Fix: Inject a clock for deterministic tests.
Interview questions
Token bucket vs sliding window?
Token bucket allows controlled bursts and uses O(1) memory per key; sliding window log is precise over the window but stores timestamps. Sliding window counters approximate with O(1) memory.
How would you make it thread-safe?
Store buckets in a concurrent map and synchronize the refill-and-consume step per bucket, or use atomic compare-and-set on packed state.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Logger rate limiter | Easy | Per-message window. |
| Token bucket with per-endpoint rules | Medium | Strategy + config. |
| Thread-safe sliding window counter | Hard | Concurrency. |