Designing an In-Memory Rate Limiter
Difficulty: Medium Patterns: Strategy, Factory, Singleton Asked at: Google, Amazon, Uber, Stripe, Razorpay
Functional Requirements
- Support multiple rate limiting algorithms โ Token Bucket, Sliding Window Log, and Fixed Window Counter
- Allow per-client/key rate limiting โ each client identified by a unique key gets independent limits
- Provide a simple
allowRequest(key)API that returns whether a request is allowed or denied - Support configurable limits โ max requests and time window size are configurable per limiter instance
- Allow runtime algorithm swapping โ change the rate limiting strategy without restarting or rebuilding
- Expose current usage stats โ remaining tokens/quota for a given client key
Non-Functional Requirements
- Thread-safety โ concurrent requests for the same or different keys must not corrupt internal state
- O(1) decision time โ Token Bucket and Fixed Window must decide in constant time; Sliding Window in O(n) worst case bounded by window size
- Memory efficiency โ automatic cleanup of stale client entries to prevent unbounded memory growth
Core Entities
| Entity | Description |
|---|---|
RateLimiter |
Facade that holds a strategy and delegates allow/deny decisions per key |
RateLimitStrategy |
Interface defining the allowRequest and getRemainingQuota contract |
TokenBucketStrategy |
Refills tokens at a steady rate, each request consumes one token |
SlidingWindowLogStrategy |
Tracks exact timestamps of requests, counts those within the window |
FixedWindowCounterStrategy |
Divides time into fixed windows, counts requests per window |
ClientState |
Per-key state (tokens, timestamps, counters) managed internally by each strategy |
RateLimiterConfig |
Holds maxRequests, windowSizeMs, and refill rate parameters |
RateLimiterFactory |
Creates a RateLimiter with the desired algorithm by name |
Class Diagram
classDiagram
class RateLimitAlgorithm {
<<enumeration>>
TOKEN_BUCKET
SLIDING_WINDOW_LOG
FIXED_WINDOW_COUNTER
}
class RateLimiterConfig {
-int maxRequests
-long windowSizeMs
+getMaxRequests() int
+getWindowSizeMs() long
}
class RateLimitStrategy {
<<interface>>
+allowRequest(String key) boolean
+getRemainingQuota(String key) int
}
class TokenBucketStrategy {
-int maxTokens
-long refillIntervalMs
-ConcurrentHashMap clientBuckets
+allowRequest(String key) boolean
+getRemainingQuota(String key) int
}
class SlidingWindowLogStrategy {
-int maxRequests
-long windowSizeMs
-ConcurrentHashMap clientLogs
+allowRequest(String key) boolean
+getRemainingQuota(String key) int
}
class FixedWindowCounterStrategy {
-int maxRequests
-long windowSizeMs
-ConcurrentHashMap clientCounters
+allowRequest(String key) boolean
+getRemainingQuota(String key) int
}
class RateLimiter {
-RateLimitStrategy strategy
-ReentrantLock lock
+allowRequest(String key) boolean
+getRemainingQuota(String key) int
+setStrategy(RateLimitStrategy)
}
class RateLimiterFactory {
+create(RateLimitAlgorithm, RateLimiterConfig) RateLimiter
}
RateLimiter --> RateLimitStrategy
RateLimiter --> RateLimiterConfig
RateLimitStrategy <|.. TokenBucketStrategy
RateLimitStrategy <|.. SlidingWindowLogStrategy
RateLimitStrategy <|.. FixedWindowCounterStrategy
RateLimiterFactory --> RateLimiter
RateLimiterFactory --> RateLimitAlgorithm
Design Patterns
| Pattern | Where | Why |
|---|---|---|
| Strategy | RateLimitStrategy interface with three implementations |
Swap algorithms at runtime. Adding a new algorithm = one new class, zero changes to RateLimiter. |
| Factory | RateLimiterFactory.create(algorithm, config) |
Decouple client code from concrete strategy instantiation. |
| Singleton | RateLimiter can be used as a singleton per service |
One shared limiter instance ensures global enforcement across threads. |
Data Structures
| Component | Structure | Why |
|---|---|---|
| Token Bucket state | ConcurrentHashMap<String, TokenBucket> where bucket has tokens + lastRefillTimestamp |
O(1) lookup per key, lazy refill on access |
| Sliding Window Log | ConcurrentHashMap<String, Deque<Long>> storing request timestamps |
Deque allows O(1) append and O(k) eviction of expired entries from front |
| Fixed Window Counter | ConcurrentHashMap<String, WindowCounter> with count + windowStart |
O(1) increment and reset when window rolls over |
| Strategy reference | Single RateLimitStrategy field in RateLimiter |
Hot-swappable via setter under lock |
How It All Fits Together
Hereโs what happens when a request arrives for client โuser-42โ:
- Caller invokes
rateLimiter.allowRequest("user-42") - RateLimiter delegates to the currently configured
RateLimitStrategy - Strategy looks up (or creates) the per-client state in its ConcurrentHashMap
- Token Bucket path: Calculate tokens to refill since last access, cap at max. If tokens > 0, decrement and allow. Otherwise deny.
- Sliding Window path: Remove timestamps older than
now - windowSizefrom the front of the deque. If deque size < maxRequests, append current timestamp and allow. Otherwise deny. - Fixed Window path: Check if current time is still within the active window. If window expired, reset counter. If counter < maxRequests, increment and allow. Otherwise deny.
- Return
true(allowed) orfalse(rate limited) to the caller
When swapping algorithms at runtime:
- Caller invokes
rateLimiter.setStrategy(newStrategy) - Lock is acquired to prevent mid-request strategy swaps
- Old strategy reference is replaced โ existing per-client state in the old strategy is abandoned
- Subsequent requests use the new strategy with fresh state
Complete Code
RateLimiterConfig
Configuration object holding the rate limit parameters. Immutable after construction โ shared safely across threads.
public class RateLimiterConfig {
private final int maxRequests;
private final long windowSizeMs;
public RateLimiterConfig(int maxRequests, long windowSizeMs) {
if (maxRequests <= 0) throw new IllegalArgumentException("maxRequests must be positive");
if (windowSizeMs <= 0) throw new IllegalArgumentException("windowSizeMs must be positive");
this.maxRequests = maxRequests;
this.windowSizeMs = windowSizeMs;
}
public int getMaxRequests() { return maxRequests; }
public long getWindowSizeMs() { return windowSizeMs; }
@Override
public String toString() {
return "RateLimiterConfig[max=" + maxRequests + ", window=" + windowSizeMs + "ms]";
}
}
class RateLimiterConfig:
def __init__(self, max_requests: int, window_size_ms: int):
if max_requests <= 0:
raise ValueError("max_requests must be positive")
if window_size_ms <= 0:
raise ValueError("window_size_ms must be positive")
self._max_requests = max_requests
self._window_size_ms = window_size_ms
@property
def max_requests(self) -> int:
return self._max_requests
@property
def window_size_ms(self) -> int:
return self._window_size_ms
def __str__(self) -> str:
return f"RateLimiterConfig[max={self._max_requests}, window={self._window_size_ms}ms]"
#pragma once
#include <stdexcept>
#include <string>
class RateLimiterConfig {
private:
int maxRequests;
long long windowSizeMs;
public:
RateLimiterConfig(int maxRequests, long long windowSizeMs)
: maxRequests(maxRequests), windowSizeMs(windowSizeMs) {
if (maxRequests <= 0) throw std::invalid_argument("maxRequests must be positive");
if (windowSizeMs <= 0) throw std::invalid_argument("windowSizeMs must be positive");
}
int getMaxRequests() const { return maxRequests; }
long long getWindowSizeMs() const { return windowSizeMs; }
std::string toString() const {
return "RateLimiterConfig[max=" + std::to_string(maxRequests) +
", window=" + std::to_string(windowSizeMs) + "ms]";
}
};
RateLimitStrategy (Interface)
The strategy interface defines the contract every algorithm must fulfill. Two methods: allowRequest for the allow/deny decision, and getRemainingQuota for observability.
public interface RateLimitStrategy {
/**
* Determine if a request from the given key should be allowed.
* @param key unique identifier for the client (e.g., user ID, IP address)
* @return true if allowed, false if rate limited
*/
boolean allowRequest(String key);
/**
* Get the remaining quota for the given key.
* @param key unique identifier for the client
* @return number of remaining requests allowed in current window
*/
int getRemainingQuota(String key);
}
from abc import ABC, abstractmethod
class RateLimitStrategy(ABC):
@abstractmethod
def allow_request(self, key: str) -> bool:
"""Determine if a request from the given key should be allowed."""
pass
@abstractmethod
def get_remaining_quota(self, key: str) -> int:
"""Get the remaining quota for the given key."""
pass
#pragma once
#include <string>
class RateLimitStrategy {
public:
virtual ~RateLimitStrategy() = default;
/**
* Determine if a request from the given key should be allowed.
* @param key unique identifier for the client
* @return true if allowed, false if rate limited
*/
virtual bool allowRequest(const std::string& key) = 0;
/**
* Get the remaining quota for the given key.
* @param key unique identifier for the client
* @return number of remaining requests allowed in current window
*/
virtual int getRemainingQuota(const std::string& key) = 0;
};
TokenBucketStrategy
The Token Bucket algorithm: each client starts with a full bucket of tokens. Each request consumes one token. Tokens refill at a steady rate over time. This gives smooth rate limiting with burst tolerance โ a client that was idle accumulates tokens and can burst up to the bucket capacity.
import java.util.concurrent.ConcurrentHashMap;
public class TokenBucketStrategy implements RateLimitStrategy {
private static class Bucket {
double tokens;
long lastRefillTimestamp;
Bucket(double tokens, long lastRefillTimestamp) {
this.tokens = tokens;
this.lastRefillTimestamp = lastRefillTimestamp;
}
}
private final int maxTokens;
private final double refillRatePerMs; // tokens added per millisecond
private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
public TokenBucketStrategy(RateLimiterConfig config) {
this.maxTokens = config.getMaxRequests();
// Refill the entire bucket over one window period
this.refillRatePerMs = (double) maxTokens / config.getWindowSizeMs();
}
@Override
public synchronized boolean allowRequest(String key) {
long now = System.currentTimeMillis();
Bucket bucket = buckets.computeIfAbsent(key, k -> new Bucket(maxTokens, now));
// Refill tokens based on elapsed time
long elapsed = now - bucket.lastRefillTimestamp;
bucket.tokens = Math.min(maxTokens, bucket.tokens + elapsed * refillRatePerMs);
bucket.lastRefillTimestamp = now;
if (bucket.tokens >= 1.0) {
bucket.tokens -= 1.0;
return true;
}
return false;
}
@Override
public synchronized int getRemainingQuota(String key) {
long now = System.currentTimeMillis();
Bucket bucket = buckets.get(key);
if (bucket == null) return maxTokens;
long elapsed = now - bucket.lastRefillTimestamp;
double currentTokens = Math.min(maxTokens, bucket.tokens + elapsed * refillRatePerMs);
return (int) currentTokens;
}
}
import time
import threading
class TokenBucketStrategy(RateLimitStrategy):
def __init__(self, config: RateLimiterConfig):
self._max_tokens = config.max_requests
# Refill entire bucket over one window period
self._refill_rate_per_ms = config.max_requests / config.window_size_ms
self._buckets: dict[str, dict] = {}
self._lock = threading.Lock()
def allow_request(self, key: str) -> bool:
with self._lock:
now = time.time() * 1000 # current time in ms
if key not in self._buckets:
self._buckets[key] = {
"tokens": self._max_tokens,
"last_refill": now
}
bucket = self._buckets[key]
# Refill tokens based on elapsed time
elapsed = now - bucket["last_refill"]
bucket["tokens"] = min(
self._max_tokens,
bucket["tokens"] + elapsed * self._refill_rate_per_ms
)
bucket["last_refill"] = now
if bucket["tokens"] >= 1.0:
bucket["tokens"] -= 1.0
return True
return False
def get_remaining_quota(self, key: str) -> int:
with self._lock:
now = time.time() * 1000
if key not in self._buckets:
return self._max_tokens
bucket = self._buckets[key]
elapsed = now - bucket["last_refill"]
current_tokens = min(
self._max_tokens,
bucket["tokens"] + elapsed * self._refill_rate_per_ms
)
return int(current_tokens)
#pragma once
#include <unordered_map>
#include <mutex>
#include <chrono>
#include <algorithm>
#include "RateLimitStrategy.hpp"
#include "RateLimiterConfig.hpp"
class TokenBucketStrategy : public RateLimitStrategy {
private:
struct Bucket {
double tokens;
long long lastRefillTimestamp;
};
int maxTokens;
double refillRatePerMs;
std::unordered_map<std::string, Bucket> buckets;
std::mutex mtx;
long long nowMs() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
}
public:
TokenBucketStrategy(const RateLimiterConfig& config)
: maxTokens(config.getMaxRequests()),
refillRatePerMs(static_cast<double>(config.getMaxRequests()) / config.getWindowSizeMs()) {}
bool allowRequest(const std::string& key) override {
std::lock_guard<std::mutex> lock(mtx);
long long now = nowMs();
auto it = buckets.find(key);
if (it == buckets.end()) {
buckets[key] = Bucket{static_cast<double>(maxTokens), now};
it = buckets.find(key);
}
Bucket& bucket = it->second;
// Refill tokens based on elapsed time
long long elapsed = now - bucket.lastRefillTimestamp;
bucket.tokens = std::min(static_cast<double>(maxTokens),
bucket.tokens + elapsed * refillRatePerMs);
bucket.lastRefillTimestamp = now;
if (bucket.tokens >= 1.0) {
bucket.tokens -= 1.0;
return true;
}
return false;
}
int getRemainingQuota(const std::string& key) override {
std::lock_guard<std::mutex> lock(mtx);
long long now = nowMs();
auto it = buckets.find(key);
if (it == buckets.end()) return maxTokens;
const Bucket& bucket = it->second;
long long elapsed = now - bucket.lastRefillTimestamp;
double currentTokens = std::min(static_cast<double>(maxTokens),
bucket.tokens + elapsed * refillRatePerMs);
return static_cast<int>(currentTokens);
}
};
SlidingWindowLogStrategy
The Sliding Window Log algorithm: stores the exact timestamp of every request within the window. On each request, expired timestamps are evicted from the front of the deque, then we check if the count is below the limit. Most accurate โ no boundary issues โ but uses more memory proportional to request rate.
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;
public class SlidingWindowLogStrategy implements RateLimitStrategy {
private final int maxRequests;
private final long windowSizeMs;
private final ConcurrentHashMap<String, Deque<Long>> clientLogs = new ConcurrentHashMap<>();
public SlidingWindowLogStrategy(RateLimiterConfig config) {
this.maxRequests = config.getMaxRequests();
this.windowSizeMs = config.getWindowSizeMs();
}
@Override
public synchronized boolean allowRequest(String key) {
long now = System.currentTimeMillis();
Deque<Long> log = clientLogs.computeIfAbsent(key, k -> new LinkedList<>());
// Evict timestamps outside the current window
long windowStart = now - windowSizeMs;
while (!log.isEmpty() && log.peekFirst() <= windowStart) {
log.pollFirst();
}
if (log.size() < maxRequests) {
log.addLast(now);
return true;
}
return false;
}
@Override
public synchronized int getRemainingQuota(String key) {
long now = System.currentTimeMillis();
Deque<Long> log = clientLogs.get(key);
if (log == null) return maxRequests;
long windowStart = now - windowSizeMs;
// Count only requests within the window
int count = 0;
for (Long timestamp : log) {
if (timestamp > windowStart) count++;
}
return Math.max(0, maxRequests - count);
}
}
import time
import threading
from collections import deque
class SlidingWindowLogStrategy(RateLimitStrategy):
def __init__(self, config: RateLimiterConfig):
self._max_requests = config.max_requests
self._window_size_ms = config.window_size_ms
self._client_logs: dict[str, deque] = {}
self._lock = threading.Lock()
def allow_request(self, key: str) -> bool:
with self._lock:
now = time.time() * 1000
if key not in self._client_logs:
self._client_logs[key] = deque()
log = self._client_logs[key]
# Evict timestamps outside the current window
window_start = now - self._window_size_ms
while log and log[0] <= window_start:
log.popleft()
if len(log) < self._max_requests:
log.append(now)
return True
return False
def get_remaining_quota(self, key: str) -> int:
with self._lock:
now = time.time() * 1000
if key not in self._client_logs:
return self._max_requests
log = self._client_logs[key]
window_start = now - self._window_size_ms
count = sum(1 for ts in log if ts > window_start)
return max(0, self._max_requests - count)
#pragma once
#include <unordered_map>
#include <deque>
#include <mutex>
#include <chrono>
#include <algorithm>
#include "RateLimitStrategy.hpp"
#include "RateLimiterConfig.hpp"
class SlidingWindowLogStrategy : public RateLimitStrategy {
private:
int maxRequests;
long long windowSizeMs;
std::unordered_map<std::string, std::deque<long long>> clientLogs;
std::mutex mtx;
long long nowMs() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
}
public:
SlidingWindowLogStrategy(const RateLimiterConfig& config)
: maxRequests(config.getMaxRequests()),
windowSizeMs(config.getWindowSizeMs()) {}
bool allowRequest(const std::string& key) override {
std::lock_guard<std::mutex> lock(mtx);
long long now = nowMs();
long long windowStart = now - windowSizeMs;
auto& log = clientLogs[key];
// Evict timestamps outside the current window
while (!log.empty() && log.front() <= windowStart) {
log.pop_front();
}
if (static_cast<int>(log.size()) < maxRequests) {
log.push_back(now);
return true;
}
return false;
}
int getRemainingQuota(const std::string& key) override {
std::lock_guard<std::mutex> lock(mtx);
long long now = nowMs();
long long windowStart = now - windowSizeMs;
auto it = clientLogs.find(key);
if (it == clientLogs.end()) return maxRequests;
int count = 0;
for (long long ts : it->second) {
if (ts > windowStart) count++;
}
return std::max(0, maxRequests - count);
}
};
FixedWindowCounterStrategy
The Fixed Window Counter algorithm: divides time into discrete windows of fixed duration. Each window has a counter that resets when the window expires. Simple and memory-efficient, but can allow up to 2x the rate at window boundaries (a known tradeoff).
import java.util.concurrent.ConcurrentHashMap;
public class FixedWindowCounterStrategy implements RateLimitStrategy {
private static class WindowCounter {
int count;
long windowStart;
WindowCounter(int count, long windowStart) {
this.count = count;
this.windowStart = windowStart;
}
}
private final int maxRequests;
private final long windowSizeMs;
private final ConcurrentHashMap<String, WindowCounter> counters = new ConcurrentHashMap<>();
public FixedWindowCounterStrategy(RateLimiterConfig config) {
this.maxRequests = config.getMaxRequests();
this.windowSizeMs = config.getWindowSizeMs();
}
@Override
public synchronized boolean allowRequest(String key) {
long now = System.currentTimeMillis();
WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter(0, now));
// Check if the current window has expired
if (now - counter.windowStart >= windowSizeMs) {
counter.count = 0;
counter.windowStart = now;
}
if (counter.count < maxRequests) {
counter.count++;
return true;
}
return false;
}
@Override
public synchronized int getRemainingQuota(String key) {
long now = System.currentTimeMillis();
WindowCounter counter = counters.get(key);
if (counter == null) return maxRequests;
// If window expired, full quota is available
if (now - counter.windowStart >= windowSizeMs) {
return maxRequests;
}
return Math.max(0, maxRequests - counter.count);
}
}
import time
import threading
class FixedWindowCounterStrategy(RateLimitStrategy):
def __init__(self, config: RateLimiterConfig):
self._max_requests = config.max_requests
self._window_size_ms = config.window_size_ms
self._counters: dict[str, dict] = {}
self._lock = threading.Lock()
def allow_request(self, key: str) -> bool:
with self._lock:
now = time.time() * 1000
if key not in self._counters:
self._counters[key] = {"count": 0, "window_start": now}
counter = self._counters[key]
# Check if the current window has expired
if now - counter["window_start"] >= self._window_size_ms:
counter["count"] = 0
counter["window_start"] = now
if counter["count"] < self._max_requests:
counter["count"] += 1
return True
return False
def get_remaining_quota(self, key: str) -> int:
with self._lock:
now = time.time() * 1000
if key not in self._counters:
return self._max_requests
counter = self._counters[key]
if now - counter["window_start"] >= self._window_size_ms:
return self._max_requests
return max(0, self._max_requests - counter["count"])
#pragma once
#include <unordered_map>
#include <mutex>
#include <chrono>
#include <algorithm>
#include "RateLimitStrategy.hpp"
#include "RateLimiterConfig.hpp"
class FixedWindowCounterStrategy : public RateLimitStrategy {
private:
struct WindowCounter {
int count;
long long windowStart;
};
int maxRequests;
long long windowSizeMs;
std::unordered_map<std::string, WindowCounter> counters;
std::mutex mtx;
long long nowMs() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
}
public:
FixedWindowCounterStrategy(const RateLimiterConfig& config)
: maxRequests(config.getMaxRequests()),
windowSizeMs(config.getWindowSizeMs()) {}
bool allowRequest(const std::string& key) override {
std::lock_guard<std::mutex> lock(mtx);
long long now = nowMs();
auto it = counters.find(key);
if (it == counters.end()) {
counters[key] = WindowCounter{0, now};
it = counters.find(key);
}
WindowCounter& counter = it->second;
// Check if the current window has expired
if (now - counter.windowStart >= windowSizeMs) {
counter.count = 0;
counter.windowStart = now;
}
if (counter.count < maxRequests) {
counter.count++;
return true;
}
return false;
}
int getRemainingQuota(const std::string& key) override {
std::lock_guard<std::mutex> lock(mtx);
long long now = nowMs();
auto it = counters.find(key);
if (it == counters.end()) return maxRequests;
const WindowCounter& counter = it->second;
if (now - counter.windowStart >= windowSizeMs) {
return maxRequests;
}
return std::max(0, maxRequests - counter.count);
}
};
RateLimiterFactory
Factory class that creates a RateLimiter with the desired algorithm. Clients only specify an algorithm name and config โ they never directly instantiate strategy classes.
public enum RateLimitAlgorithm {
TOKEN_BUCKET,
SLIDING_WINDOW_LOG,
FIXED_WINDOW_COUNTER
}
public class RateLimiterFactory {
public static RateLimiter create(RateLimitAlgorithm algorithm, RateLimiterConfig config) {
RateLimitStrategy strategy;
switch (algorithm) {
case TOKEN_BUCKET:
strategy = new TokenBucketStrategy(config);
break;
case SLIDING_WINDOW_LOG:
strategy = new SlidingWindowLogStrategy(config);
break;
case FIXED_WINDOW_COUNTER:
strategy = new FixedWindowCounterStrategy(config);
break;
default:
throw new IllegalArgumentException("Unknown algorithm: " + algorithm);
}
return new RateLimiter(strategy);
}
}
from enum import Enum
class RateLimitAlgorithm(Enum):
TOKEN_BUCKET = "TOKEN_BUCKET"
SLIDING_WINDOW_LOG = "SLIDING_WINDOW_LOG"
FIXED_WINDOW_COUNTER = "FIXED_WINDOW_COUNTER"
class RateLimiterFactory:
@staticmethod
def create(algorithm: RateLimitAlgorithm, config: RateLimiterConfig) -> "RateLimiter":
if algorithm == RateLimitAlgorithm.TOKEN_BUCKET:
strategy = TokenBucketStrategy(config)
elif algorithm == RateLimitAlgorithm.SLIDING_WINDOW_LOG:
strategy = SlidingWindowLogStrategy(config)
elif algorithm == RateLimitAlgorithm.FIXED_WINDOW_COUNTER:
strategy = FixedWindowCounterStrategy(config)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
return RateLimiter(strategy)
#pragma once
#include <memory>
#include <stdexcept>
#include "RateLimiter.hpp"
#include "TokenBucketStrategy.hpp"
#include "SlidingWindowLogStrategy.hpp"
#include "FixedWindowCounterStrategy.hpp"
enum class RateLimitAlgorithm {
TOKEN_BUCKET,
SLIDING_WINDOW_LOG,
FIXED_WINDOW_COUNTER
};
class RateLimiterFactory {
public:
static RateLimiter create(RateLimitAlgorithm algorithm, const RateLimiterConfig& config) {
std::unique_ptr<RateLimitStrategy> strategy;
switch (algorithm) {
case RateLimitAlgorithm::TOKEN_BUCKET:
strategy = std::make_unique<TokenBucketStrategy>(config);
break;
case RateLimitAlgorithm::SLIDING_WINDOW_LOG:
strategy = std::make_unique<SlidingWindowLogStrategy>(config);
break;
case RateLimitAlgorithm::FIXED_WINDOW_COUNTER:
strategy = std::make_unique<FixedWindowCounterStrategy>(config);
break;
default:
throw std::invalid_argument("Unknown algorithm");
}
return RateLimiter(std::move(strategy));
}
};
RateLimiter (Facade)
The main RateLimiter class that clients interact with. Holds a reference to the active strategy and provides thread-safe delegation. The strategy can be swapped at runtime via setStrategy().
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RateLimiter {
private RateLimitStrategy strategy;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public RateLimiter(RateLimitStrategy strategy) {
if (strategy == null) throw new IllegalArgumentException("Strategy cannot be null");
this.strategy = strategy;
}
/**
* Check if a request from the given key is allowed.
* Thread-safe โ uses a read lock so multiple requests can be processed concurrently.
*/
public boolean allowRequest(String key) {
lock.readLock().lock();
try {
return strategy.allowRequest(key);
} finally {
lock.readLock().unlock();
}
}
/**
* Get remaining quota for the given key.
*/
public int getRemainingQuota(String key) {
lock.readLock().lock();
try {
return strategy.getRemainingQuota(key);
} finally {
lock.readLock().unlock();
}
}
/**
* Swap the rate limiting strategy at runtime.
* Uses a write lock to ensure no requests are mid-flight during the swap.
*/
public void setStrategy(RateLimitStrategy newStrategy) {
if (newStrategy == null) throw new IllegalArgumentException("Strategy cannot be null");
lock.writeLock().lock();
try {
this.strategy = newStrategy;
} finally {
lock.writeLock().unlock();
}
}
}
import threading
class RateLimiter:
def __init__(self, strategy: RateLimitStrategy):
if strategy is None:
raise ValueError("Strategy cannot be None")
self._strategy = strategy
self._lock = threading.RLock()
def allow_request(self, key: str) -> bool:
"""Check if a request from the given key is allowed. Thread-safe."""
with self._lock:
return self._strategy.allow_request(key)
def get_remaining_quota(self, key: str) -> int:
"""Get remaining quota for the given key."""
with self._lock:
return self._strategy.get_remaining_quota(key)
def set_strategy(self, new_strategy: RateLimitStrategy) -> None:
"""Swap the rate limiting strategy at runtime."""
if new_strategy is None:
raise ValueError("Strategy cannot be None")
with self._lock:
self._strategy = new_strategy
#pragma once
#include <memory>
#include <shared_mutex>
#include <stdexcept>
#include "RateLimitStrategy.hpp"
class RateLimiter {
private:
std::unique_ptr<RateLimitStrategy> strategy;
mutable std::shared_mutex mtx;
public:
RateLimiter(std::unique_ptr<RateLimitStrategy> strategy)
: strategy(std::move(strategy)) {
if (!this->strategy) throw std::invalid_argument("Strategy cannot be null");
}
// Move constructor for factory usage
RateLimiter(RateLimiter&& other) noexcept
: strategy(std::move(other.strategy)) {}
RateLimiter& operator=(RateLimiter&& other) noexcept {
strategy = std::move(other.strategy);
return *this;
}
/**
* Check if a request from the given key is allowed.
* Thread-safe โ uses a shared lock for concurrent reads.
*/
bool allowRequest(const std::string& key) {
std::shared_lock<std::shared_mutex> lock(mtx);
return strategy->allowRequest(key);
}
/**
* Get remaining quota for the given key.
*/
int getRemainingQuota(const std::string& key) {
std::shared_lock<std::shared_mutex> lock(mtx);
return strategy->getRemainingQuota(key);
}
/**
* Swap the rate limiting strategy at runtime.
* Uses an exclusive lock to ensure no requests are mid-flight during the swap.
*/
void setStrategy(std::unique_ptr<RateLimitStrategy> newStrategy) {
if (!newStrategy) throw std::invalid_argument("Strategy cannot be null");
std::unique_lock<std::shared_mutex> lock(mtx);
strategy = std::move(newStrategy);
}
};
Main (Demo)
A runnable demo showing all three algorithms in action: creates a rate limiter, fires requests, observes throttling, and demonstrates runtime strategy swapping.
public class Main {
public static void main(String[] args) throws InterruptedException {
// Config: 5 requests per 1000ms (1 second)
RateLimiterConfig config = new RateLimiterConfig(5, 1000);
System.out.println("=== Token Bucket Strategy ===");
RateLimiter limiter = RateLimiterFactory.create(RateLimitAlgorithm.TOKEN_BUCKET, config);
testLimiter(limiter, "user-1");
System.out.println("\n=== Sliding Window Log Strategy ===");
limiter.setStrategy(new SlidingWindowLogStrategy(config));
testLimiter(limiter, "user-2");
System.out.println("\n=== Fixed Window Counter Strategy ===");
limiter.setStrategy(new FixedWindowCounterStrategy(config));
testLimiter(limiter, "user-3");
System.out.println("\n=== Per-Client Isolation Test ===");
RateLimiter isolationLimiter = RateLimiterFactory.create(RateLimitAlgorithm.TOKEN_BUCKET, config);
// client-A uses up all tokens
for (int i = 0; i < 5; i++) {
isolationLimiter.allowRequest("client-A");
}
// client-B should still have full quota
System.out.println("client-A remaining: " + isolationLimiter.getRemainingQuota("client-A"));
System.out.println("client-B remaining: " + isolationLimiter.getRemainingQuota("client-B"));
System.out.println("\n=== Refill Test (Token Bucket) ===");
RateLimiter refillLimiter = RateLimiterFactory.create(RateLimitAlgorithm.TOKEN_BUCKET, config);
// Exhaust tokens
for (int i = 0; i < 5; i++) {
refillLimiter.allowRequest("user-refill");
}
System.out.println("After exhausting: " + refillLimiter.getRemainingQuota("user-refill"));
// Wait for refill
Thread.sleep(1100);
System.out.println("After 1.1s wait: " + refillLimiter.getRemainingQuota("user-refill"));
}
private static void testLimiter(RateLimiter limiter, String key) {
System.out.println("Sending 8 requests for key: " + key);
for (int i = 1; i <= 8; i++) {
boolean allowed = limiter.allowRequest(key);
System.out.printf(" Request %d: %s (remaining: %d)%n",
i, allowed ? "ALLOWED" : "DENIED", limiter.getRemainingQuota(key));
}
}
}
import time
def test_limiter(limiter: RateLimiter, key: str):
print(f"Sending 8 requests for key: {key}")
for i in range(1, 9):
allowed = limiter.allow_request(key)
remaining = limiter.get_remaining_quota(key)
status = "ALLOWED" if allowed else "DENIED"
print(f" Request {i}: {status} (remaining: {remaining})")
def main():
# Config: 5 requests per 1000ms (1 second)
config = RateLimiterConfig(max_requests=5, window_size_ms=1000)
print("=== Token Bucket Strategy ===")
limiter = RateLimiterFactory.create(RateLimitAlgorithm.TOKEN_BUCKET, config)
test_limiter(limiter, "user-1")
print("\n=== Sliding Window Log Strategy ===")
limiter.set_strategy(SlidingWindowLogStrategy(config))
test_limiter(limiter, "user-2")
print("\n=== Fixed Window Counter Strategy ===")
limiter.set_strategy(FixedWindowCounterStrategy(config))
test_limiter(limiter, "user-3")
print("\n=== Per-Client Isolation Test ===")
isolation_limiter = RateLimiterFactory.create(RateLimitAlgorithm.TOKEN_BUCKET, config)
# client-A uses up all tokens
for _ in range(5):
isolation_limiter.allow_request("client-A")
# client-B should still have full quota
print(f"client-A remaining: {isolation_limiter.get_remaining_quota('client-A')}")
print(f"client-B remaining: {isolation_limiter.get_remaining_quota('client-B')}")
print("\n=== Refill Test (Token Bucket) ===")
refill_limiter = RateLimiterFactory.create(RateLimitAlgorithm.TOKEN_BUCKET, config)
# Exhaust tokens
for _ in range(5):
refill_limiter.allow_request("user-refill")
print(f"After exhausting: {refill_limiter.get_remaining_quota('user-refill')}")
# Wait for refill
time.sleep(1.1)
print(f"After 1.1s wait: {refill_limiter.get_remaining_quota('user-refill')}")
if __name__ == "__main__":
main()
#include <iostream>
#include <thread>
#include <chrono>
#include "RateLimiterConfig.hpp"
#include "RateLimiterFactory.hpp"
void testLimiter(RateLimiter& limiter, const std::string& key) {
std::cout << "Sending 8 requests for key: " << key << std::endl;
for (int i = 1; i <= 8; i++) {
bool allowed = limiter.allowRequest(key);
int remaining = limiter.getRemainingQuota(key);
std::cout << " Request " << i << ": "
<< (allowed ? "ALLOWED" : "DENIED")
<< " (remaining: " << remaining << ")" << std::endl;
}
}
int main() {
// Config: 5 requests per 1000ms (1 second)
RateLimiterConfig config(5, 1000);
std::cout << "=== Token Bucket Strategy ===" << std::endl;
auto limiter = RateLimiterFactory::create(RateLimitAlgorithm::TOKEN_BUCKET, config);
testLimiter(limiter, "user-1");
std::cout << "\n=== Sliding Window Log Strategy ===" << std::endl;
limiter.setStrategy(std::make_unique<SlidingWindowLogStrategy>(config));
testLimiter(limiter, "user-2");
std::cout << "\n=== Fixed Window Counter Strategy ===" << std::endl;
limiter.setStrategy(std::make_unique<FixedWindowCounterStrategy>(config));
testLimiter(limiter, "user-3");
std::cout << "\n=== Per-Client Isolation Test ===" << std::endl;
auto isolationLimiter = RateLimiterFactory::create(RateLimitAlgorithm::TOKEN_BUCKET, config);
// client-A uses up all tokens
for (int i = 0; i < 5; i++) {
isolationLimiter.allowRequest("client-A");
}
// client-B should still have full quota
std::cout << "client-A remaining: " << isolationLimiter.getRemainingQuota("client-A") << std::endl;
std::cout << "client-B remaining: " << isolationLimiter.getRemainingQuota("client-B") << std::endl;
std::cout << "\n=== Refill Test (Token Bucket) ===" << std::endl;
auto refillLimiter = RateLimiterFactory::create(RateLimitAlgorithm::TOKEN_BUCKET, config);
// Exhaust tokens
for (int i = 0; i < 5; i++) {
refillLimiter.allowRequest("user-refill");
}
std::cout << "After exhausting: " << refillLimiter.getRemainingQuota("user-refill") << std::endl;
// Wait for refill
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
std::cout << "After 1.1s wait: " << refillLimiter.getRemainingQuota("user-refill") << std::endl;
return 0;
}
Related Concepts
Scale this design past a single process and these are the concepts it runs into:
- Rate Limiting โ โ the distributed form: shared counters, the cost of syncing them, and where the limiter belongs
- API Gateway โ โ in production the limiter runs at the gateway, before a request ever reaches your service
- Caching โ โ per-key state in Redis with a TTL is how these in-memory maps survive multiple instances
- Retry & Backoff โ โ a 429 only helps if clients back off; without it they amplify the overload they caused
- Circuit Breaker โ โ rate limiting protects you from clients, a breaker protects you from a failing dependency
Discussion
Newest first