"Design a rate limiter that restricts how many requests a client can make to an API within a given time window. It should support multiple limiting algorithms and different limits for different clients or endpoints."
B. Difficulty
Intermediate for a single-process, single-algorithm version. Escalates to Advanced + Distributed Systems the moment the interviewer asks "now run this across many servers" — at which point the interesting problem stops being OOD and becomes "how do multiple processes atomically share one counter."
C. What This Problem Tests
Strategy pattern — this is arguably the single most natural Strategy example in all of LLD, since the algorithm (token bucket vs. sliding window vs. fixed window) is explicitly expected to vary
Factory pattern for producing the right configured limiter per endpoint/client tier
Testability discipline — passing time in rather than reading a wall clock internally
Concurrency — atomic check-and-decrement under a shared counter
Recognizing the LLD→distributed-systems boundary
D. Requirements
Functional
Accept a request identified by client ID (and optionally endpoint)
Decide allow/deny based on a configured limit and algorithm
Support multiple algorithms (token bucket, sliding window, fixed window) interchangeably
Return enough information for the caller to act: allowed/denied, remaining quota, retry-after
Non-Functional
Decision latency must be low — this sits on the hot path of every request
Thread-safe under concurrent requests from the same client
Extensible to new algorithms without touching the calling code
Bounded memory even with millions of distinct clients (stale state must be reclaimable)
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Is limiting per-client, per-client-per-endpoint, per-IP, or global?
Defines the key granularity for tracked state
The composite key used in the bucket store
2
Which algorithm(s) must be supported — token bucket, sliding window, fixed window, leaky bucket?
Different algorithms have different burst/precision/memory trade-offs
Strategy interface shape
3
Can different endpoints/clients have different limits and different algorithms?
Drives whether configuration is global or per-key
Factory + config design
4
Is this a single process, or must it work correctly across multiple servers behind a load balancer?
The single biggest scoping question — changes this from an LLD problem to a distributed-systems one
In-memory map vs. shared store (Redis)
5
On deny, should the request be rejected outright, or queued/delayed?
Affects whether the API is purely synchronous allow/deny
Method signature — blocking vs. non-blocking
6
Should the response expose remaining quota and retry-after?
Common real-world requirement (HTTP 429 headers)
Return type richness (RateLimitResult vs. plain boolean)
7
Should the clock be injectable rather than read internally?
Determines whether algorithms are deterministically testable
Method signature includes a timestamp parameter
8
What's the default policy for a brand-new/unknown client?
Fail-open (unlimited until first violation) vs. fail-closed (deny by default)
Default config lookup behavior
9
Must limits be updatable at runtime without a restart?
Affects whether config is baked in at construction or re-read per request
Hot-reloadable config store
10
Is short-burst traffic acceptable, or must throughput be perfectly smooth?
Token bucket allows bursts; sliding window is stricter
Default algorithm choice
11
How many distinct clients must be tracked simultaneously?
Millions of clients means per-client state must be evictable
TTL/eviction on idle client buckets
F. Assumptions
Confirmed: must support multiple algorithms interchangeably; different clients/endpoints can have different limits.
Assumption: per-client-per-endpoint granularity as the default key.
Assumption: single-process, in-memory design first (V1–V5); distributed sharing of state is an explicit extension (V6), not baked in from the start.
Assumption: timestamps are passed into algorithm methods (server-generated, never trusted from the client) rather than read from a wall clock internally, for testability and to prevent client clock manipulation.
Assumption: idle client state is evicted after a TTL to bound memory.
G. Actors
Actor
Responsibility
Client
Sends requests; is the subject being rate-limited
API Gateway / Service
Calls the rate limiter before processing each request; acts on allow/deny
Admin/Operator
Configures per-client/per-endpoint limits (outside the hot path)
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
RateLimiter (facade)
Public entry point; resolves the right algorithm + config for a key and delegates
—
check(clientId, endpoint, now)
Algorithm math itself
RateLimitAlgorithm (interface)
Decides allow/deny for one client key, given a timestamp
Produces the correctly configured algorithm instance per endpoint
—
getLimiter(endpoint)
Making the allow/deny decision itself
RateLimitConfig
Declarative limit definition (capacity, window, algorithm type) per endpoint/tier
—
—
Any behavior — pure data
RateLimitResult
Structured decision returned to the caller
allowed, remaining, retryAfterMs
—
Any behavior — pure data
I. Class Relationships
Dependency (interface):RateLimiter → RateLimitAlgorithm. The facade never knows which concrete algorithm it's talking to.
Creation:RateLimiterFactory builds algorithm instances from RateLimitConfig — this is where "which algorithm for which endpoint" is decided, kept out of both the facade and the algorithms themselves.
Composition: each algorithm instance owns its own per-client state map — state and algorithm travel together, since token bucket state (tokens, lastRefill) is structurally different from sliding-window state (a timestamp deque).
Why not one shared generic "Bucket" object across all algorithms? Because the state shape genuinely differs per algorithm (a single counter vs. a queue of timestamps vs. two rolling counters) — forcing a common state class would either bloat it with unused fields or leak abstraction, similar to the LRU/LFU issue in Problem 2.
Factory resolves config; algorithms only do their own math; facade only delegates
One class reading config, doing token-bucket math, and sliding-window math via internal if/else
OCP
Add a new algorithm = new class implementing RateLimitAlgorithm; zero edits elsewhere
if (algoType == "TOKEN_BUCKET") ... else if (algoType == "SLIDING_WINDOW") ... inside one giant method
LSP
Any algorithm can substitute for another behind allowRequest() without special-casing by caller
An algorithm that needs extra caller-side setup the interface doesn't declare
ISP
One-method interface — no algorithm forced to implement irrelevant methods
A fat interface with resetWindow(), getBurstCapacity(), etc. that fixed-window doesn't need
DIP
RateLimiter depends on the RateLimitAlgorithm abstraction, not a concrete class
RateLimiter directly instantiating new TokenBucketLimiter() inline
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
Strategy
The algorithm is explicitly expected to vary and be swapped
RateLimitAlgorithm + implementations
N/A — this is the textbook case; almost never wrong to use it here
Factory
Choosing the right configured algorithm per endpoint without scattering config-reading logic
RateLimiterFactory
Skip if there's genuinely only one global algorithm/config — a factory with one product is ceremony
Facade
Callers shouldn't need to know about factories/configs/algorithms
RateLimiter.check()
Skip only if the system is trivially small
Decorator (optional)
Add logging/metrics around every check without touching algorithm code
A MetricsRateLimiter wrapping any RateLimitAlgorithm
Skip unless observability was actually requested
Explicitly avoided: a Chain-of-Responsibility for "try token bucket, then sliding window, then fixed window until one matches" — there's no such fallback semantic here; a direct factory lookup by config is simpler and correct. Chain-of-Responsibility is tempting-sounding but not justified by the requirements.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Algorithm
Token bucket
Sliding window, leaky bucket
RateLimitAlgorithm interface
Limit granularity
Per-client-per-endpoint
Per-IP, per-API-key, global
Composite key construction isolated in one place
State storage
In-memory map
Shared store (Redis) for multi-server correctness
State access hidden behind the algorithm class, not exposed to callers
Limit values
Static config
Runtime-adjustable, per-tier (free/premium)
RateLimitConfig resolved fresh per request, not cached at construction
O. Design Evolution
V1: one hardcoded fixed-window algorithm, one global limit.
V2: per-client tracking — a map keyed by client ID.
V3: extract RateLimitAlgorithm; add token bucket and sliding window as swappable strategies.
V4: per-endpoint/per-tier configuration via RateLimiterFactory + RateLimitConfig.
V5: thread-safety — per-client lock (or atomic state) so the hot path doesn't serialize unrelated clients.
V6: distributed — move per-client state into a shared store (Redis), using an atomic script (e.g. Lua) for the check-and-decrement so multiple app servers agree on one counter; define a fail-open/fail-closed policy for when that store is unreachable.
What stays stable:RateLimiter.check()'s signature. V6's biggest change is entirely inside the algorithm implementations' storage layer.
P. API / Interface Design
interface RateLimitAlgorithm {
RateLimitResult allowRequest(String clientKey, long nowMs);
}
class RateLimiter {
RateLimitResult check(String clientId, String endpoint, long nowMs);
}
class RateLimitResult {
boolean allowed;
long remaining;
long retryAfterMs; // 0 if allowed
}
interface RateLimitAlgorithm {
RateLimitResult allowRequest(String clientKey, long nowMs);
}
class RateLimiter {
RateLimitResult check(String clientId, String endpoint, long nowMs) { throw new UnsupportedOperationException(); }
}
class RateLimitResult {
boolean allowed;
long remaining;
long retryAfterMs; // 0 if allowed
}
class RateLimitAlgorithm {
public:
virtual RateLimitResult allowRequest(const std::string& clientKey, long nowMs) = 0;
};
class RateLimiter {
public:
RateLimitResult check(const std::string& clientId, const std::string& endpoint, long nowMs);
};
struct RateLimitResult {
bool allowed;
long remaining;
long retryAfterMs; // 0 if allowed
};
class RateLimitAlgorithm {
allowRequest(clientKey, nowMs) { throw new Error('not implemented'); }
}
class RateLimiter {
check(clientId, endpoint, nowMs) { throw new Error('not implemented'); }
}
class RateLimitResult {
constructor(allowed, remaining, retryAfterMs) {
this.allowed = allowed;
this.remaining = remaining;
this.retryAfterMs = retryAfterMs; // 0 if allowed
}
}
interface RateLimitAlgorithm {
allowRequest(clientKey: string, nowMs: number): RateLimitResult;
}
class RateLimiter {
check(clientId: string, endpoint: string, nowMs: number): RateLimitResult { throw new Error('not implemented'); }
}
class RateLimitResult {
allowed: boolean;
remaining: number;
retryAfterMs: number; // 0 if allowed
}
Ownership/validation notes:nowMs is always supplied by the calling layer (server-side clock), never trusted from client input, and never read internally via a static call — this is what makes every algorithm implementation independently unit-testable with fabricated timestamps.
O(1) access, safe concurrent inserts of new client keys
A single global lock around a plain map — simpler, but serializes all clients
Deque/queue of timestamps (sliding window log)
Exact sliding-window counting
Precise — no boundary burst issue
Fixed window counters — O(1) memory but allows a 2x burst at window boundaries
Two rolling counters (sliding window counter, approximate)
Memory-efficient approximation of sliding window
O(1) memory per client instead of O(requests in window)
Less precise than the full log approach — accepted trade-off at scale
T. Complexity Analysis
Algorithm
Time per Request
Space per Client
Precision
Token Bucket
O(1)
O(1)
Allows controlled bursts
Fixed Window Counter
O(1)
O(1)
Boundary issue — up to 2x burst across a window edge
Sliding Window Log
O(w) to prune (w = requests in window)
O(w)
Exact
Sliding Window Counter (approx.)
O(1)
O(1)
Close approximation, small error at boundaries
U. Testing Strategy
Happy path: requests under the limit are all allowed.
Edge cases: exactly at the limit; burst then wait exactly the refill interval (deterministic via injected clock); window-boundary burst for fixed-window (intentionally demonstrate the known weakness).
Invalid input: unknown client falls back to the configured default policy.
Concurrency tests: N threads racing for the last token — exactly capacity requests succeed, never more.
Failure scenarios: shared store (if distributed) times out — verify the configured fail-open/fail-closed behavior triggers correctly.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Run this across 10 servers behind a load balancer."
LLD → distributed boundary
Move per-client state to a shared store (Redis); use an atomic script for check-and-decrement
Storage layer inside each algorithm impl
Fail-open vs. fail-closed when the store is unreachable
"Different limits for free vs. premium users."
Config-driven variability, not code changes
RateLimitConfig keyed by tier; Factory selects per-request
RateLimiterFactory, RateLimitConfig
Zero change to algorithm classes — validates the OCP investment
"Two requests for the last token arrive simultaneously."
Atomicity of check-then-decrement
Guard the refill+check+decrement sequence with a per-key lock (or CAS loop)
Concrete algorithm class
Per-key lock beats a single global lock for throughput
"Limits must be changeable without a restart."
Hot-reload awareness
Config lookup happens per-request from a live config store, not cached at startup
RateLimiterFactory
Adds a small per-request config-lookup cost — usually worth it
"Millions of distinct clients — memory is exploding."
Bounded-resource thinking
TTL-evict idle client buckets (same idea as LRU eviction in Problem 2)
Per-algorithm state store
Trades a little memory precision for a bounded footprint
"What if a client sends its own timestamp to try to reset its limit?"
Security/trust boundary awareness
Never use client-supplied time for algorithm math — timestamp must be server-generated
API contract (already designed this way — validates the earlier decision)
Trust boundary discipline
"The shared Redis cluster goes down."
Failure-mode reasoning
Explicit, documented fail-open (risk overload) or fail-closed (risk false rejections) policy — not left undefined
Facade-level fallback logic
Availability vs. protection trade-off — there's no universally "correct" choice, only an explicit one
"Add a global limit on top of per-endpoint limits."
Composability of rules
Check a global limiter first, then the per-endpoint one, short-circuiting on the first denial
RateLimiter.check() orchestration
Ordering matters — decide and document which check runs first
"Return exactly how long until the client can retry."
API completeness
RateLimitResult.retryAfterMs, computed differently per algorithm
Already part of the base design — validates early API choice
—
W. Concurrency
Shared mutable state: each client's bucket/window counters. The canonical race: two threads both read "tokens = 1," both decide to allow, both decrement — exceeding the limit by one.
Fine-grained locking: lock per client key (not one global lock) so unrelated clients never contend — critical for throughput at scale.
Atomic alternative: if the state is simple enough (e.g. a single counter in fixed-window), an atomic compare-and-swap loop can avoid locking entirely; token bucket's compound refill+decrement is harder to express as a single atomic op, so a lock is the more honest answer there.
Distributed atomicity: once state lives in Redis, "check-then-decrement" must be one atomic operation from Redis's perspective — a Lua script (executed atomically server-side) is the standard tool, rather than a separate GET then SET from the application, which would reintroduce the same race across the network.
Idempotency: rate-limit checks are naturally idempotent-safe (they're not a mutating business action) — a retried check just consumes an extra unit of quota, which is an acceptable, well-understood cost, not a correctness bug.
X. Persistence
Rate-limiter state is inherently ephemeral and derived — losing it on restart (resetting all clients to a full bucket) is an acceptable, safe default, not a data-loss incident. In the distributed design, Redis (with a TTL) effectively serves as the "shared state" layer, but it's not meant to be a durable system of record — it can be treated as a fast, shared cache.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Algorithm classes, Factory, in-process state and locking for one server
HLD
Where the rate limiter sits (gateway/middleware), a shared Redis/Memcached tier, per-service vs. centralized limiting
Distributed Systems
Atomic cross-node counter updates (Lua scripts), replication of the shared store, fail-open/fail-closed policy under partition, clock considerations across nodes
Z. Failure Scenarios
Shared store outage (distributed mode): must have an explicit fail-open/fail-closed policy — silence here is itself a bug.
Duplicate/retried client requests: naturally handled — each retry is just another checked request, no special idempotency machinery needed since rate-limiting isn't a mutating operation.
Clock skew across distributed nodes: mitigated by centralizing the time-sensitive math inside the shared store's atomic script rather than trusting each app server's local clock independently.
Stale/leaked per-client state: bounded via TTL eviction — otherwise an attacker could cheaply exhaust memory by rotating fake client IDs.
AA. Common Bad Designs
One class hardcoding token-bucket math with no interface — quick to write, but adding sliding-window later means editing and re-testing already-shipped, working code — the textbook OCP violation this problem exists to teach.
A single global lock around the entire limiter — "looks" thread-safe, but serializes every client's requests behind every other client's, destroying throughput.
Trusting a client-supplied timestamp — looks convenient for testing, but is a real security hole: a client could manipulate its own rate limit.
Building the distributed (Redis) version on day one
AB. Design Trade-Offs
Decision
Option A
Option B
Trade-off
Algorithm
Token Bucket
Sliding Window Log
Token bucket allows bursts and is O(1) memory; sliding window log is exact but O(requests-in-window) memory
Locking
Global lock
Per-client lock
Global is simpler to reason about; per-client scales far better under load
State location
In-memory (single server)
Shared store (Redis)
In-memory is fast and simple but wrong once there's more than one server; shared store is correct at scale but adds latency and a new failure mode
Failure policy
Fail-open
Fail-closed
Fail-open protects availability at the cost of temporarily unlimited traffic; fail-closed protects the backend at the cost of rejecting legitimate traffic during an outage
AC. Interviewer Evaluation
Weak candidate: hardcodes one algorithm inline with no interface; doesn't ask about per-client vs. global scope.
Average candidate: extracts a Strategy interface for algorithms, but doesn't discuss concurrency or mention testability (clock injection).
Strong candidate: Strategy + Factory, proactively raises the check-and-decrement race, chooses per-client locking, and injects time as a parameter without being asked.
Exceptional candidate: all of the above, and when scale comes up, can immediately articulate the fail-open/fail-closed trade-off and why Redis needs an atomic script rather than separate GET/SET calls.
AD. Interview Simulation
Interviewer: Design a rate limiter for an API.
Candidate: Should limiting be per-client, per-endpoint, or both — and do you need multiple algorithms supported, like token bucket versus sliding window?
Interviewer: Per-client-per-endpoint, and yes, assume the algorithm might change later.
Candidate: Then I'll define a RateLimitAlgorithm interface with one method, allowRequest(key, timestamp) — the timestamp is passed in rather than read internally, so every algorithm is testable without mocking a clock.
Interviewer: Good. Implement token bucket.
Candidate: Each client key maps to a bucket with a token count and last-refill time. On each request, I refill based on elapsed time, then check if a token is available before decrementing.
Interviewer: Two requests for the same client arrive at the same instant, both see one token left.
Candidate: That refill-check-decrement sequence needs to be atomic per client key — I'd use a lock scoped to that client's bucket, not a global lock, so unrelated clients aren't affected.
Interviewer: Now this needs to run correctly across ten application servers.
Candidate: At that point the per-client state has to move out of any single server's memory into a shared store like Redis, and the check-and-decrement needs to be one atomic operation there too — typically a Lua script executed server-side, so two app servers can't race each other over the network.
Interviewer: What if Redis goes down?
Candidate: That's a policy decision, not a bug to hide — fail-open keeps the service available but risks a traffic spike; fail-closed protects the backend but rejects legitimate users. I'd make that explicit and configurable rather than picking silently.
Hardcoded algorithm with no interface; global lock; trusting client clocks; premature distributed design
What NOT to over-engineer
Don't build the Redis/distributed version before the interviewer asks for scale
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Rate Limiter - full LLD implementation (single-file demo).
// Difficulty badge is "Intermediate -> Advanced+Distributed" - it does NOT explicitly say
// "Concurrency", so per convention this stays single-threaded/simple (no locks), even
// though a production system would eventually need per-key locking or a distributed store.
import java.util.*;
public class p3 {
// ---------- RateLimitResult (pure data) ----------
static class RateLimitResult {
final boolean allowed;
final long remaining;
final long retryAfterMs; // 0 if allowed
RateLimitResult(boolean allowed, long remaining, long retryAfterMs) {
this.allowed = allowed;
this.remaining = remaining;
this.retryAfterMs = retryAfterMs;
}
static RateLimitResult allow(long remaining) { return new RateLimitResult(true, remaining, 0); }
static RateLimitResult deny(long retryAfterMs) { return new RateLimitResult(false, 0, retryAfterMs); }
public String toString() {
return "RateLimitResult{allowed=" + allowed + ", remaining=" + remaining + ", retryAfterMs=" + retryAfterMs + "}";
}
}
// ---------- RateLimitConfig (pure data) ----------
enum AlgorithmType { TOKEN_BUCKET, SLIDING_WINDOW, FIXED_WINDOW }
static class RateLimitConfig {
final long capacity; // max tokens / max requests per window
final long windowMs; // window size in ms (or refill window for token bucket)
final AlgorithmType algorithmType;
RateLimitConfig(long capacity, long windowMs, AlgorithmType algorithmType) {
this.capacity = capacity;
this.windowMs = windowMs;
this.algorithmType = algorithmType;
}
}
// ---------- RateLimitAlgorithm (interface) ----------
interface RateLimitAlgorithm {
RateLimitResult allowRequest(String clientKey, long nowMs);
}
// Each algorithm owns its own per-client state map, since the state shape genuinely
// differs (a counter vs. a timestamp deque vs. two rolling counters) - forcing a shared
// generic "Bucket" class would either bloat it or leak abstraction.
static class TokenBucketLimiter implements RateLimitAlgorithm {
private static class Bucket {
double tokens;
long lastRefillTime;
}
private final long capacity;
private final double refillRatePerMs; // tokens added per millisecond
private final Map<String, Bucket> buckets = new HashMap<>();
TokenBucketLimiter(RateLimitConfig config) {
this.capacity = config.capacity;
this.refillRatePerMs = (double) config.capacity / config.windowMs;
}
public RateLimitResult allowRequest(String clientKey, long nowMs) {
Bucket bucket = buckets.computeIfAbsent(clientKey, k -> {
Bucket b = new Bucket();
b.tokens = capacity;
b.lastRefillTime = nowMs;
return b;
});
long elapsed = nowMs - bucket.lastRefillTime;
bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * refillRatePerMs);
bucket.lastRefillTime = nowMs;
if (bucket.tokens >= 1) {
bucket.tokens -= 1;
return RateLimitResult.allow((long) Math.floor(bucket.tokens));
}
double deficit = 1 - bucket.tokens;
long retryAfterMs = (long) Math.ceil(deficit / refillRatePerMs);
return RateLimitResult.deny(retryAfterMs);
}
}
// Sliding window: keeps an exact deque of recent request timestamps per client and
// counts how many fall within the last windowMs - more accurate than fixed window at
// the cost of memory proportional to request rate.
static class SlidingWindowLimiter implements RateLimitAlgorithm {
private final long capacity;
private final long windowMs;
private final Map<String, Deque<Long>> history = new HashMap<>();
SlidingWindowLimiter(RateLimitConfig config) {
this.capacity = config.capacity;
this.windowMs = config.windowMs;
}
public RateLimitResult allowRequest(String clientKey, long nowMs) {
Deque<Long> timestamps = history.computeIfAbsent(clientKey, k -> new ArrayDeque<>());
long windowStart = nowMs - windowMs;
while (!timestamps.isEmpty() && timestamps.peekFirst() <= windowStart) {
timestamps.pollFirst();
}
if (timestamps.size() < capacity) {
timestamps.addLast(nowMs);
return RateLimitResult.allow(capacity - timestamps.size());
}
long retryAfterMs = timestamps.peekFirst() + windowMs - nowMs;
return RateLimitResult.deny(Math.max(0, retryAfterMs));
}
}
// Fixed window: counts requests in discrete, non-overlapping windows - cheap (one
// counter per client) but allows bursts at window boundaries.
static class FixedWindowLimiter implements RateLimitAlgorithm {
private static class WindowCounter {
long windowStart;
long count;
}
private final long capacity;
private final long windowMs;
private final Map<String, WindowCounter> counters = new HashMap<>();
FixedWindowLimiter(RateLimitConfig config) {
this.capacity = config.capacity;
this.windowMs = config.windowMs;
}
public RateLimitResult allowRequest(String clientKey, long nowMs) {
WindowCounter counter = counters.computeIfAbsent(clientKey, k -> {
WindowCounter c = new WindowCounter();
c.windowStart = nowMs;
c.count = 0;
return c;
});
if (nowMs - counter.windowStart >= windowMs) {
counter.windowStart = nowMs;
counter.count = 0;
}
if (counter.count < capacity) {
counter.count++;
return RateLimitResult.allow(capacity - counter.count);
}
long retryAfterMs = counter.windowStart + windowMs - nowMs;
return RateLimitResult.deny(Math.max(0, retryAfterMs));
}
}
// ---------- RateLimiterFactory ----------
// Resolves "which algorithm for which endpoint" - kept out of both the facade and the
// algorithms themselves so that mapping logic has exactly one home.
static class RateLimiterFactory {
private final Map<String, RateLimitConfig> configsByEndpoint;
private final Map<String, RateLimitAlgorithm> instancesByEndpoint = new HashMap<>();
RateLimiterFactory(Map<String, RateLimitConfig> configsByEndpoint) {
this.configsByEndpoint = configsByEndpoint;
}
RateLimitAlgorithm getLimiter(String endpoint) {
return instancesByEndpoint.computeIfAbsent(endpoint, ep -> {
RateLimitConfig config = configsByEndpoint.get(ep);
if (config == null) throw new IllegalArgumentException("No config for endpoint: " + ep);
switch (config.algorithmType) {
case TOKEN_BUCKET: return new TokenBucketLimiter(config);
case SLIDING_WINDOW: return new SlidingWindowLimiter(config);
case FIXED_WINDOW: return new FixedWindowLimiter(config);
default: throw new IllegalStateException("Unknown algorithm type");
}
});
}
}
// ---------- RateLimiter (facade) ----------
static class RateLimiter {
private final RateLimiterFactory factory; // injected interface-like dependency
RateLimiter(RateLimiterFactory factory) {
this.factory = factory;
}
RateLimitResult check(String clientId, String endpoint, long nowMs) {
RateLimitAlgorithm algorithm = factory.getLimiter(endpoint);
String clientKey = endpoint + ":" + clientId; // scope quota per endpoint+client
return algorithm.allowRequest(clientKey, nowMs);
}
}
// ---------- Demo ----------
public static void main(String[] args) {
Map<String, RateLimitConfig> configs = new HashMap<>();
configs.put("/api/login", new RateLimitConfig(3, 1000, AlgorithmType.TOKEN_BUCKET));
configs.put("/api/search", new RateLimitConfig(2, 1000, AlgorithmType.FIXED_WINDOW));
RateLimiterFactory factory = new RateLimiterFactory(configs);
RateLimiter limiter = new RateLimiter(factory);
long t = 0;
for (int i = 0; i < 5; i++) {
RateLimitResult result = limiter.check("clientA", "/api/login", t);
System.out.println("t=" + t + " login attempt " + (i + 1) + " -> " + result);
t += 100; // 100ms between requests, faster than refill can keep up
}
System.out.println();
long t2 = 0;
for (int i = 0; i < 3; i++) {
RateLimitResult result = limiter.check("clientB", "/api/search", t2);
System.out.println("t=" + t2 + " search attempt " + (i + 1) + " -> " + result);
}
}
}
// Rate Limiter - full LLD implementation (single-file demo).
// Difficulty badge is "Intermediate -> Advanced+Distributed" - it does NOT explicitly say
// "Concurrency", so per convention this stays single-threaded/simple (no mutexes), even
// though a production system would eventually need per-key locking or a distributed store.
#include <iostream>
#include <unordered_map>
#include <deque>
#include <string>
#include <memory>
#include <cmath>
#include <stdexcept>
// ---------- RateLimitResult (pure data) ----------
struct RateLimitResult {
bool allowed;
long long remaining;
long long retryAfterMs; // 0 if allowed
static RateLimitResult allow(long long remaining) { return {true, remaining, 0}; }
static RateLimitResult deny(long long retryAfterMs) { return {false, 0, retryAfterMs}; }
};
std::ostream& operator<<(std::ostream& os, const RateLimitResult& r) {
os << "RateLimitResult{allowed=" << (r.allowed ? "true" : "false")
<< ", remaining=" << r.remaining << ", retryAfterMs=" << r.retryAfterMs << "}";
return os;
}
// ---------- RateLimitConfig (pure data) ----------
enum class AlgorithmType { TOKEN_BUCKET, SLIDING_WINDOW, FIXED_WINDOW };
struct RateLimitConfig {
long long capacity; // max tokens / max requests per window
long long windowMs; // window size in ms (or refill window for token bucket)
AlgorithmType algorithmType;
};
// ---------- RateLimitAlgorithm (interface) ----------
class RateLimitAlgorithm {
public:
virtual ~RateLimitAlgorithm() = default;
virtual RateLimitResult allowRequest(const std::string& clientKey, long long nowMs) = 0;
};
// Each algorithm owns its own per-client state map, since the state shape genuinely
// differs (a counter vs. a timestamp deque vs. two rolling counters) - forcing a shared
// generic "Bucket" class would either bloat it or leak abstraction.
class TokenBucketLimiter : public RateLimitAlgorithm {
public:
explicit TokenBucketLimiter(const RateLimitConfig& config)
: capacity(config.capacity), refillRatePerMs(static_cast<double>(config.capacity) / config.windowMs) {}
RateLimitResult allowRequest(const std::string& clientKey, long long nowMs) override {
Bucket& bucket = buckets[clientKey];
if (!bucket.initialized) {
bucket.tokens = static_cast<double>(capacity);
bucket.lastRefillTime = nowMs;
bucket.initialized = true;
}
long long elapsed = nowMs - bucket.lastRefillTime;
bucket.tokens = std::min(static_cast<double>(capacity), bucket.tokens + elapsed * refillRatePerMs);
bucket.lastRefillTime = nowMs;
if (bucket.tokens >= 1.0) {
bucket.tokens -= 1.0;
return RateLimitResult::allow(static_cast<long long>(std::floor(bucket.tokens)));
}
double deficit = 1.0 - bucket.tokens;
long long retryAfterMs = static_cast<long long>(std::ceil(deficit / refillRatePerMs));
return RateLimitResult::deny(retryAfterMs);
}
private:
struct Bucket {
double tokens = 0;
long long lastRefillTime = 0;
bool initialized = false;
};
long long capacity;
double refillRatePerMs;
std::unordered_map<std::string, Bucket> buckets;
};
// Sliding window: keeps an exact deque of recent request timestamps per client and
// counts how many fall within the last windowMs - more accurate than fixed window at
// the cost of memory proportional to request rate.
class SlidingWindowLimiter : public RateLimitAlgorithm {
public:
explicit SlidingWindowLimiter(const RateLimitConfig& config)
: capacity(config.capacity), windowMs(config.windowMs) {}
RateLimitResult allowRequest(const std::string& clientKey, long long nowMs) override {
std::deque<long long>& timestamps = history[clientKey];
long long windowStart = nowMs - windowMs;
while (!timestamps.empty() && timestamps.front() <= windowStart) {
timestamps.pop_front();
}
if (static_cast<long long>(timestamps.size()) < capacity) {
timestamps.push_back(nowMs);
return RateLimitResult::allow(capacity - static_cast<long long>(timestamps.size()));
}
long long retryAfterMs = timestamps.front() + windowMs - nowMs;
return RateLimitResult::deny(std::max<long long>(0, retryAfterMs));
}
private:
long long capacity;
long long windowMs;
std::unordered_map<std::string, std::deque<long long>> history;
};
// Fixed window: counts requests in discrete, non-overlapping windows - cheap (one
// counter per client) but allows bursts at window boundaries.
class FixedWindowLimiter : public RateLimitAlgorithm {
public:
explicit FixedWindowLimiter(const RateLimitConfig& config)
: capacity(config.capacity), windowMs(config.windowMs) {}
RateLimitResult allowRequest(const std::string& clientKey, long long nowMs) override {
WindowCounter& counter = counters[clientKey];
if (!counter.initialized) {
counter.windowStart = nowMs;
counter.count = 0;
counter.initialized = true;
}
if (nowMs - counter.windowStart >= windowMs) {
counter.windowStart = nowMs;
counter.count = 0;
}
if (counter.count < capacity) {
counter.count++;
return RateLimitResult::allow(capacity - counter.count);
}
long long retryAfterMs = counter.windowStart + windowMs - nowMs;
return RateLimitResult::deny(std::max<long long>(0, retryAfterMs));
}
private:
struct WindowCounter {
long long windowStart = 0;
long long count = 0;
bool initialized = false;
};
long long capacity;
long long windowMs;
std::unordered_map<std::string, WindowCounter> counters;
};
// ---------- RateLimiterFactory ----------
// Resolves "which algorithm for which endpoint" - kept out of both the facade and the
// algorithms themselves so that mapping logic has exactly one home.
class RateLimiterFactory {
public:
explicit RateLimiterFactory(std::unordered_map<std::string, RateLimitConfig> configsByEndpoint)
: configsByEndpoint(std::move(configsByEndpoint)) {}
RateLimitAlgorithm* getLimiter(const std::string& endpoint) {
auto it = instancesByEndpoint.find(endpoint);
if (it != instancesByEndpoint.end()) return it->second.get();
auto configIt = configsByEndpoint.find(endpoint);
if (configIt == configsByEndpoint.end()) throw std::invalid_argument("No config for endpoint: " + endpoint);
const RateLimitConfig& config = configIt->second;
std::unique_ptr<RateLimitAlgorithm> algorithm;
switch (config.algorithmType) {
case AlgorithmType::TOKEN_BUCKET: algorithm = std::make_unique<TokenBucketLimiter>(config); break;
case AlgorithmType::SLIDING_WINDOW: algorithm = std::make_unique<SlidingWindowLimiter>(config); break;
case AlgorithmType::FIXED_WINDOW: algorithm = std::make_unique<FixedWindowLimiter>(config); break;
}
RateLimitAlgorithm* raw = algorithm.get();
instancesByEndpoint[endpoint] = std::move(algorithm);
return raw;
}
private:
std::unordered_map<std::string, RateLimitConfig> configsByEndpoint;
std::unordered_map<std::string, std::unique_ptr<RateLimitAlgorithm>> instancesByEndpoint;
};
// ---------- RateLimiter (facade) ----------
class RateLimiter {
public:
explicit RateLimiter(RateLimiterFactory* factory) : factory(factory) {}
RateLimitResult check(const std::string& clientId, const std::string& endpoint, long long nowMs) {
RateLimitAlgorithm* algorithm = factory->getLimiter(endpoint);
std::string clientKey = endpoint + ":" + clientId; // scope quota per endpoint+client
return algorithm->allowRequest(clientKey, nowMs);
}
private:
RateLimiterFactory* factory; // injected dependency, not owned
};
// ---------- Demo ----------
int main() {
std::unordered_map<std::string, RateLimitConfig> configs;
configs["/api/login"] = {3, 1000, AlgorithmType::TOKEN_BUCKET};
configs["/api/search"] = {2, 1000, AlgorithmType::FIXED_WINDOW};
RateLimiterFactory factory(configs);
RateLimiter limiter(&factory);
long long t = 0;
for (int i = 0; i < 5; i++) {
RateLimitResult result = limiter.check("clientA", "/api/login", t);
std::cout << "t=" << t << " login attempt " << (i + 1) << " -> " << result << "\n";
t += 100; // 100ms between requests, faster than refill can keep up
}
std::cout << "\n";
long long t2 = 0;
for (int i = 0; i < 3; i++) {
RateLimitResult result = limiter.check("clientB", "/api/search", t2);
std::cout << "t=" << t2 << " search attempt " << (i + 1) << " -> " << result << "\n";
}
return 0;
}
// Rate Limiter - full LLD implementation (single-file demo).
// Difficulty badge is "Intermediate -> Advanced+Distributed" - it does NOT explicitly say
// "Concurrency", so per convention this stays single-threaded/simple (no locks), even
// though a production system would eventually need per-key locking or a distributed store.
// ---------- RateLimitResult (pure data) ----------
class RateLimitResult {
constructor(allowed, remaining, retryAfterMs) {
this.allowed = allowed;
this.remaining = remaining;
this.retryAfterMs = retryAfterMs; // 0 if allowed
}
static allow(remaining) { return new RateLimitResult(true, remaining, 0); }
static deny(retryAfterMs) { return new RateLimitResult(false, 0, retryAfterMs); }
}
// ---------- RateLimitConfig (pure data) ----------
const AlgorithmType = { TOKEN_BUCKET: "TOKEN_BUCKET", SLIDING_WINDOW: "SLIDING_WINDOW", FIXED_WINDOW: "FIXED_WINDOW" };
class RateLimitConfig {
constructor(capacity, windowMs, algorithmType) {
this.capacity = capacity; // max tokens / max requests per window
this.windowMs = windowMs; // window size in ms (or refill window for token bucket)
this.algorithmType = algorithmType;
}
}
// ---------- RateLimitAlgorithm (interface) ----------
class RateLimitAlgorithm {
allowRequest(_clientKey, _nowMs) {
throw new Error("allowRequest() must be implemented");
}
}
// Each algorithm owns its own per-client state map, since the state shape genuinely
// differs (a counter vs. a timestamp deque vs. two rolling counters) - forcing a shared
// generic "Bucket" class would either bloat it or leak abstraction.
class TokenBucketLimiter extends RateLimitAlgorithm {
constructor(config) {
super();
this.capacity = config.capacity;
this.refillRatePerMs = config.capacity / config.windowMs; // tokens added per ms
this.buckets = new Map(); // clientKey -> { tokens, lastRefillTime }
}
allowRequest(clientKey, nowMs) {
let bucket = this.buckets.get(clientKey);
if (!bucket) {
bucket = { tokens: this.capacity, lastRefillTime: nowMs };
this.buckets.set(clientKey, bucket);
}
const elapsed = nowMs - bucket.lastRefillTime;
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed * this.refillRatePerMs);
bucket.lastRefillTime = nowMs;
if (bucket.tokens >= 1) {
bucket.tokens -= 1;
return RateLimitResult.allow(Math.floor(bucket.tokens));
}
const deficit = 1 - bucket.tokens;
const retryAfterMs = Math.ceil(deficit / this.refillRatePerMs);
return RateLimitResult.deny(retryAfterMs);
}
}
// Sliding window: keeps an exact queue of recent request timestamps per client and
// counts how many fall within the last windowMs - more accurate than fixed window at
// the cost of memory proportional to request rate.
class SlidingWindowLimiter extends RateLimitAlgorithm {
constructor(config) {
super();
this.capacity = config.capacity;
this.windowMs = config.windowMs;
this.history = new Map(); // clientKey -> array of timestamps (oldest first)
}
allowRequest(clientKey, nowMs) {
let timestamps = this.history.get(clientKey);
if (!timestamps) { timestamps = []; this.history.set(clientKey, timestamps); }
const windowStart = nowMs - this.windowMs;
while (timestamps.length > 0 && timestamps[0] <= windowStart) {
timestamps.shift();
}
if (timestamps.length < this.capacity) {
timestamps.push(nowMs);
return RateLimitResult.allow(this.capacity - timestamps.length);
}
const retryAfterMs = timestamps[0] + this.windowMs - nowMs;
return RateLimitResult.deny(Math.max(0, retryAfterMs));
}
}
// Fixed window: counts requests in discrete, non-overlapping windows - cheap (one
// counter per client) but allows bursts at window boundaries.
class FixedWindowLimiter extends RateLimitAlgorithm {
constructor(config) {
super();
this.capacity = config.capacity;
this.windowMs = config.windowMs;
this.counters = new Map(); // clientKey -> { windowStart, count }
}
allowRequest(clientKey, nowMs) {
let counter = this.counters.get(clientKey);
if (!counter) {
counter = { windowStart: nowMs, count: 0 };
this.counters.set(clientKey, counter);
}
if (nowMs - counter.windowStart >= this.windowMs) {
counter.windowStart = nowMs;
counter.count = 0;
}
if (counter.count < this.capacity) {
counter.count += 1;
return RateLimitResult.allow(this.capacity - counter.count);
}
const retryAfterMs = counter.windowStart + this.windowMs - nowMs;
return RateLimitResult.deny(Math.max(0, retryAfterMs));
}
}
// ---------- RateLimiterFactory ----------
// Resolves "which algorithm for which endpoint" - kept out of both the facade and the
// algorithms themselves so that mapping logic has exactly one home.
class RateLimiterFactory {
constructor(configsByEndpoint) {
this.configsByEndpoint = configsByEndpoint; // Map<endpoint, RateLimitConfig>
this.instancesByEndpoint = new Map();
}
getLimiter(endpoint) {
let instance = this.instancesByEndpoint.get(endpoint);
if (instance) return instance;
const config = this.configsByEndpoint.get(endpoint);
if (!config) throw new Error(`No config for endpoint: ${endpoint}`);
switch (config.algorithmType) {
case AlgorithmType.TOKEN_BUCKET: instance = new TokenBucketLimiter(config); break;
case AlgorithmType.SLIDING_WINDOW: instance = new SlidingWindowLimiter(config); break;
case AlgorithmType.FIXED_WINDOW: instance = new FixedWindowLimiter(config); break;
default: throw new Error("Unknown algorithm type");
}
this.instancesByEndpoint.set(endpoint, instance);
return instance;
}
}
// ---------- RateLimiter (facade) ----------
class RateLimiter {
constructor(factory) {
this.factory = factory; // injected dependency
}
check(clientId, endpoint, nowMs) {
const algorithm = this.factory.getLimiter(endpoint);
const clientKey = `${endpoint}:${clientId}`; // scope quota per endpoint+client
return algorithm.allowRequest(clientKey, nowMs);
}
}
// ---------- Demo ----------
function main() {
const configs = new Map([
["/api/login", new RateLimitConfig(3, 1000, AlgorithmType.TOKEN_BUCKET)],
["/api/search", new RateLimitConfig(2, 1000, AlgorithmType.FIXED_WINDOW)],
]);
const factory = new RateLimiterFactory(configs);
const limiter = new RateLimiter(factory);
let t = 0;
for (let i = 0; i < 5; i++) {
const result = limiter.check("clientA", "/api/login", t);
console.log(`t=${t} login attempt ${i + 1} ->`, result);
t += 100; // 100ms between requests, faster than refill can keep up
}
console.log();
let t2 = 0;
for (let i = 0; i < 3; i++) {
const result = limiter.check("clientB", "/api/search", t2);
console.log(`t=${t2} search attempt ${i + 1} ->`, result);
}
}
if (require.main === module) {
main();
}
module.exports = {
RateLimitResult, RateLimitConfig, AlgorithmType, RateLimitAlgorithm,
TokenBucketLimiter, SlidingWindowLimiter, FixedWindowLimiter,
RateLimiterFactory, RateLimiter,
};
// Rate Limiter - full LLD implementation (single-file demo).
// Difficulty badge is "Intermediate -> Advanced+Distributed" - it does NOT explicitly say
// "Concurrency", so per convention this stays single-threaded/simple (no locks), even
// though a production system would eventually need per-key locking or a distributed store.
// ---------- RateLimitResult (pure data) ----------
class RateLimitResult {
private constructor(
public readonly allowed: boolean,
public readonly remaining: number,
public readonly retryAfterMs: number // 0 if allowed
) {}
static allow(remaining: number): RateLimitResult { return new RateLimitResult(true, remaining, 0); }
static deny(retryAfterMs: number): RateLimitResult { return new RateLimitResult(false, 0, retryAfterMs); }
}
// ---------- RateLimitConfig (pure data) ----------
enum AlgorithmType { TOKEN_BUCKET = "TOKEN_BUCKET", SLIDING_WINDOW = "SLIDING_WINDOW", FIXED_WINDOW = "FIXED_WINDOW" }
class RateLimitConfig {
constructor(
public readonly capacity: number, // max tokens / max requests per window
public readonly windowMs: number, // window size in ms (or refill window for token bucket)
public readonly algorithmType: AlgorithmType
) {}
}
// ---------- RateLimitAlgorithm (interface) ----------
interface RateLimitAlgorithm {
allowRequest(clientKey: string, nowMs: number): RateLimitResult;
}
// Each algorithm owns its own per-client state map, since the state shape genuinely
// differs (a counter vs. a timestamp deque vs. two rolling counters) - forcing a shared
// generic "Bucket" class would either bloat it or leak abstraction.
class TokenBucketLimiter implements RateLimitAlgorithm {
private readonly capacity: number;
private readonly refillRatePerMs: number; // tokens added per millisecond
private readonly buckets = new Map<string, { tokens: number; lastRefillTime: number }>();
constructor(config: RateLimitConfig) {
this.capacity = config.capacity;
this.refillRatePerMs = config.capacity / config.windowMs;
}
allowRequest(clientKey: string, nowMs: number): RateLimitResult {
let bucket = this.buckets.get(clientKey);
if (!bucket) {
bucket = { tokens: this.capacity, lastRefillTime: nowMs };
this.buckets.set(clientKey, bucket);
}
const elapsed = nowMs - bucket.lastRefillTime;
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed * this.refillRatePerMs);
bucket.lastRefillTime = nowMs;
if (bucket.tokens >= 1) {
bucket.tokens -= 1;
return RateLimitResult.allow(Math.floor(bucket.tokens));
}
const deficit = 1 - bucket.tokens;
const retryAfterMs = Math.ceil(deficit / this.refillRatePerMs);
return RateLimitResult.deny(retryAfterMs);
}
}
// Sliding window: keeps an exact queue of recent request timestamps per client and
// counts how many fall within the last windowMs - more accurate than fixed window at
// the cost of memory proportional to request rate.
class SlidingWindowLimiter implements RateLimitAlgorithm {
private readonly capacity: number;
private readonly windowMs: number;
private readonly history = new Map<string, number[]>();
constructor(config: RateLimitConfig) {
this.capacity = config.capacity;
this.windowMs = config.windowMs;
}
allowRequest(clientKey: string, nowMs: number): RateLimitResult {
let timestamps = this.history.get(clientKey);
if (!timestamps) { timestamps = []; this.history.set(clientKey, timestamps); }
const windowStart = nowMs - this.windowMs;
while (timestamps.length > 0 && timestamps[0] <= windowStart) {
timestamps.shift();
}
if (timestamps.length < this.capacity) {
timestamps.push(nowMs);
return RateLimitResult.allow(this.capacity - timestamps.length);
}
const retryAfterMs = timestamps[0] + this.windowMs - nowMs;
return RateLimitResult.deny(Math.max(0, retryAfterMs));
}
}
// Fixed window: counts requests in discrete, non-overlapping windows - cheap (one
// counter per client) but allows bursts at window boundaries.
class FixedWindowLimiter implements RateLimitAlgorithm {
private readonly capacity: number;
private readonly windowMs: number;
private readonly counters = new Map<string, { windowStart: number; count: number }>();
constructor(config: RateLimitConfig) {
this.capacity = config.capacity;
this.windowMs = config.windowMs;
}
allowRequest(clientKey: string, nowMs: number): RateLimitResult {
let counter = this.counters.get(clientKey);
if (!counter) {
counter = { windowStart: nowMs, count: 0 };
this.counters.set(clientKey, counter);
}
if (nowMs - counter.windowStart >= this.windowMs) {
counter.windowStart = nowMs;
counter.count = 0;
}
if (counter.count < this.capacity) {
counter.count += 1;
return RateLimitResult.allow(this.capacity - counter.count);
}
const retryAfterMs = counter.windowStart + this.windowMs - nowMs;
return RateLimitResult.deny(Math.max(0, retryAfterMs));
}
}
// ---------- RateLimiterFactory ----------
// Resolves "which algorithm for which endpoint" - kept out of both the facade and the
// algorithms themselves so that mapping logic has exactly one home.
class RateLimiterFactory {
private readonly instancesByEndpoint = new Map<string, RateLimitAlgorithm>();
constructor(private readonly configsByEndpoint: Map<string, RateLimitConfig>) {}
getLimiter(endpoint: string): RateLimitAlgorithm {
let instance = this.instancesByEndpoint.get(endpoint);
if (instance) return instance;
const config = this.configsByEndpoint.get(endpoint);
if (!config) throw new Error(`No config for endpoint: ${endpoint}`);
switch (config.algorithmType) {
case AlgorithmType.TOKEN_BUCKET: instance = new TokenBucketLimiter(config); break;
case AlgorithmType.SLIDING_WINDOW: instance = new SlidingWindowLimiter(config); break;
case AlgorithmType.FIXED_WINDOW: instance = new FixedWindowLimiter(config); break;
default: throw new Error("Unknown algorithm type");
}
this.instancesByEndpoint.set(endpoint, instance);
return instance;
}
}
// ---------- RateLimiter (facade) ----------
class RateLimiter {
constructor(private readonly factory: RateLimiterFactory) {} // injected dependency
check(clientId: string, endpoint: string, nowMs: number): RateLimitResult {
const algorithm = this.factory.getLimiter(endpoint);
const clientKey = `${endpoint}:${clientId}`; // scope quota per endpoint+client
return algorithm.allowRequest(clientKey, nowMs);
}
}
// ---------- Demo ----------
function main(): void {
const configs = new Map<string, RateLimitConfig>([
["/api/login", new RateLimitConfig(3, 1000, AlgorithmType.TOKEN_BUCKET)],
["/api/search", new RateLimitConfig(2, 1000, AlgorithmType.FIXED_WINDOW)],
]);
const factory = new RateLimiterFactory(configs);
const limiter = new RateLimiter(factory);
let t = 0;
for (let i = 0; i < 5; i++) {
const result = limiter.check("clientA", "/api/login", t);
console.log(`t=${t} login attempt ${i + 1} ->`, result);
t += 100; // 100ms between requests, faster than refill can keep up
}
console.log();
let t2 = 0;
for (let i = 0; i < 3; i++) {
const result = limiter.check("clientB", "/api/search", t2);
console.log(`t=${t2} search attempt ${i + 1} ->`, result);
}
}
main();
export {
RateLimitResult, RateLimitConfig, AlgorithmType, RateLimitAlgorithm,
TokenBucketLimiter, SlidingWindowLimiter, FixedWindowLimiter,
RateLimiterFactory, RateLimiter,
};