Design a Distributed Rate Limiter
1. Problem Statement & Scope Clarification
System Mission
Design a distributed, highly available, ultra-low-latency Rate Limiter service (similar to AWS WAF, Cloudflare Rate Limiting, and Stripe's multi-tier throttling gateway) capable of protecting API infrastructure from volumetric DDoS attacks, credential stuffing, scraping, and downstream database resource starvation across multi-tenant microservices.
Functional Requirements
- Decision Latency & Check API (
isAllowed): Evaluate in () whether an incoming request is permitted based on client identity, endpoint, and token weight. - Multi-Dimensional Rule Evaluation: Support hierarchical throttling policies:
- Layer 3/4 & 7 IP-based rate limiting (volumetric perimeter defense).
- Per-Tenant / Per-User-ID limits (tiered SLA enforcement, e.g., Free vs Enterprise tier).
- Per-Route / Per-Method cost weighting (e.g.,
GET /userscost = 1;POST /transfers/exportcost = 10).
- Standardized HTTP 429 Compliance: Return standard headers
X-RateLimit-Limit,X-RateLimit-Remaining, andRetry-After(in seconds). - Dynamic Rule Reloading: Allow security administrators to update rate limits in real time without restarting middleware clusters or flushing counters.
Non-Functional Requirements (SLAs & SLOs)
- High Availability: uptime SLA across multi-AZ AWS regions.
- Fail-Open Policy (
FAIL_OPEN): If the rate-limiting tier experiences complete network isolation or Redis cluster failure, user traffic MUST be allowed through to prevent self-inflicted global outages. - Low Decision Latency: , .
- Memory Footprint: Strict memory per active tracking entity.
2. Capacity & Scale Estimation (Back-of-the-Envelope Math)
Traffic & Generation Scale
- Global Peak Request Volume: ().
- Active Tracked Entities (Daily Active Keys): unique client keys (
IP,API_Key,UserID). - Peak Throughput per Region: Assuming 3 active AWS regions, peak per-region traffic .
Memory Footprint Derivation
Using the Token Bucket algorithm, each client tracking record in Redis requires:
- Key name:
rl:{usr_998124}:/api/v1/orders - Hash fields (
tokensas float64,last_refreshedas int64): - Redis Hash overhead (dict entry, jemalloc alignment):
- Total per Key: .
With a Redis cluster replication factor of 2 (Primary + Replica) and a memory buffer for key eviction overhead:
Redis Cluster Node Sizing
A single Redis thread handles .
We provision an AWS ElastiCache Redis Cluster with 3 primary shards and 3 read replicas (cache.r6g.xlarge with 26.32 GiB RAM per node), providing headroom for traffic spikes.
3. Multi-Tier Defense Architecture & AWS Component Mapping
Interactive Architecture DiagramSynthesizing vector architecture diagram...
4. API Interface Design & Wire Protocol
Rate Limiter Internal Check Protocol (ratelimit.proto)
protobufsyntax = "proto3"; package hispeeddesign.ratelimit.v1; service RateLimiterService { // Evaluates rate limit status for a given key and token cost rpc CheckRateLimit (RateLimitRequest) returns (RateLimitResponse); } message RateLimitRequest { string tenant_id = 1; // e.g. "org_apple" string client_identifier = 2;// e.g. "usr_9981" or "ip_192.0.2.1" string route = 3; // e.g. "/v1/transfers" int32 token_cost = 4; // Default: 1 } message RateLimitResponse { enum Decision { ALLOWED = 0; THROTTLED = 1; FAILED_OPEN = 2; // Emitted if Redis was unreachable } Decision decision = 1; int64 limit = 2; // Maximum bucket capacity int64 remaining = 3; // Remaining available tokens int64 reset_after_seconds = 4; // Seconds until bucket fully refills string violation_reason = 5; }
5. Storage Engine & Atomic Lua Implementation
To prevent race conditions without acquiring distributed locks, token bucket state is evaluated and mutated atomically inside Redis using a single Lua script.
Production Token Bucket Lua Script (token_bucket.lua)
lua-- KEYS[1]: Rate limit key, e.g., "rl:{tenant_123}:/api/v1/orders" -- ARGV[1]: Max bucket capacity (burst limit), e.g., 100 -- ARGV[2]: Refill rate per second (sustained limit), e.g., 10 -- ARGV[3]: Token cost for this request, e.g., 1 -- ARGV[4]: Current timestamp (Unix epoch in seconds with microsecond float), e.g., 1767225600.125 -- ARGV[5]: Key TTL in seconds, e.g., 3600 local key = KEYS[1] local capacity = tonumber(ARGV[1]) local refill_rate = tonumber(ARGV[2]) local cost = tonumber(ARGV[3]) local now = tonumber(ARGV[4]) local ttl = tonumber(ARGV[5]) -- 1. Retrieve existing bucket state local data = redis.call("HMGET", key, "tokens", "last_updated") local tokens = tonumber(data[1]) local last_updated = tonumber(data[2]) if tokens == nil then -- Bucket does not exist yet: initialize to full capacity minus current cost tokens = capacity last_updated = now else -- 2. Compute newly accumulated tokens based on elapsed time local elapsed = math.max(0, now - last_updated) tokens = math.min(capacity, tokens + (elapsed * refill_rate)) last_updated = now end -- 3. Evaluate request allowance local allowed = 0 local remaining = tokens local retry_after = 0 if tokens >= cost then allowed = 1 tokens = tokens - cost remaining = math.floor(tokens) else allowed = 0 remaining = 0 -- Time required to accumulate enough tokens for this request cost retry_after = math.ceil((cost - tokens) / refill_rate) end -- 4. Persist updated bucket state back to Redis atomically redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated) redis.call("EXPIRE", key, ttl) return { allowed, remaining, capacity, retry_after }
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~46%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.