116
LVL 04 — SENIOR-IN-TRAININGSESSION 116DAY 116

RATE LIMITING

🎫 PIXELCRAFT-102
🔒Security | 🔴 Expert | Priority: 🟠 High

A single user made 50,000 API requests in one minute — crashing the server for everyone. Our basic rate limiter uses fixed windows, causing traffic spikes at window boundaries. Build a sophisticated sliding window rate limiter with Redis. 100 req/min for free users, 1000 for premium. Proper headers and graceful degradation.
CONCEPTS.UNLOCKED
🪟
Fixed Window
Count requests per calendar minute. Simple but flawed: 100 requests at 0:59 + 100 at 1:00 = 200 in 2 seconds. The "boundary burst" problem. Easy to implement, poor at preventing spikes.
📐
Sliding Window
Weight the previous window by overlap. At 1:30, count: (current window) + (previous window × 0.5). Smooth transition between windows. No boundary bursts. PixelCraft's choice.
🪣
Token Bucket
Tokens refill at a steady rate. Each request costs one token. Bucket holds max N tokens. Allows bursts up to N, then throttles to refill rate. Used by AWS, Stripe, and most cloud APIs.
💧
Leaky Bucket
Requests queue and drain at a fixed rate. Like water through a hole: input can vary, output is constant. Smoothest traffic shaping. Used in network routers and message queues.
👤
Per-User vs Per-IP
Authenticated: limit per user ID. Anonymous: limit per IP. Per-user is fairer (shared office = shared IP). Per-IP is the fallback for unauthenticated endpoints. Use both for defense in depth.
📋
Rate Limit Headers
Tell clients their limits and remaining quota. X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After. Good API citizenship: clients can throttle themselves before hitting 429.
HANDS-ON.TASKS
01
Sliding Window Counter in Redis
// lib/rateLimiter.ts import Redis from 'ioredis'; const redis = new Redis( process.env.REDIS_URL!); interface RateLimitResult { allowed: boolean; limit: number; remaining: number; resetAt: number; // epoch seconds retryAfter?: number; // seconds } async function slidingWindowCheck( key: string, limit: number, windowSec: number, ): Promise<RateLimitResult> { const now = Date.now(); const windowMs = windowSec * 1000; const currentWindow = Math.floor(now / windowMs); const previousWindow = currentWindow - 1; // Keys for current and previous const currKey = `rl:${key}:${currentWindow}`; const prevKey = `rl:${key}:${previousWindow}`; // Atomic: increment current, // read previous const pipe = redis.pipeline(); pipe.incr(currKey); pipe.expire(currKey, windowSec * 2); pipe.get(prevKey); const results = await pipe.exec(); const currCount = (results![0][1] as number) || 0; const prevCount = parseInt(results![2][1] as string || '0'); // Sliding window weight: // how far into current window? const elapsed = now - currentWindow * windowMs; const weight = 1 - elapsed / windowMs; // Weighted count const count = Math.floor(prevCount * weight) + currCount; const resetAt = Math.ceil( (currentWindow + 1) * windowMs / 1000); if (count > limit) { return { allowed: false, limit, remaining: 0, resetAt, retryAfter: resetAt - Math.floor(now / 1000), }; } return { allowed: true, limit, remaining: Math.max(0, limit - count), resetAt, }; }
02
Tiered Limits by Plan
// config/rateLimits.ts const RATE_LIMITS = { free: { requestsPerMinute: 100, uploadsPerHour: 20, aiEnhancePerDay: 5, }, premium: { requestsPerMinute: 1000, uploadsPerHour: 200, aiEnhancePerDay: 100, }, enterprise: { requestsPerMinute: 10000, uploadsPerHour: 2000, aiEnhancePerDay: -1, // unlimited }, } as const; function getLimits(user?: User) { if (!user) return RATE_LIMITS.free; return RATE_LIMITS[user.plan] ?? RATE_LIMITS.free; }
03
Express Middleware
// middleware/rateLimit.ts function rateLimitMiddleware( getLimitKey: (req: Request) => { key: string; limit: number; window: number } ) { return async ( req: Request, res: Response, next: NextFunction ) => { const { key, limit, window } = getLimitKey(req); const result = await slidingWindowCheck( key, limit, window); // Always set headers res.set({ 'X-RateLimit-Limit': String(result.limit), 'X-RateLimit-Remaining': String(result.remaining), 'X-RateLimit-Reset': String(result.resetAt), }); if (!result.allowed) { res.set('Retry-After', String(result.retryAfter)); return res.status(429).json({ error: 'Too Many Requests', retryAfter: result.retryAfter, upgradeUrl: '/pricing', }); } next(); }; } // Apply to API routes: app.use('/api', rateLimitMiddleware((req) => { const user = req.user; const limits = getLimits(user); return { key: user?.id ?? req.ip, limit: limits.requestsPerMinute, window: 60, }; }) );
04
Endpoint-Specific Limits
// Expensive endpoints get stricter // limits app.post('/api/images/upload', rateLimitMiddleware((req) => ({ key: `upload:${req.user?.id ?? req.ip}`, limit: getLimits(req.user) .uploadsPerHour, window: 3600, // 1 hour })), uploadHandler ); app.post('/api/images/ai-enhance', rateLimitMiddleware((req) => ({ key: `ai:${req.user?.id ?? req.ip}`, limit: getLimits(req.user) .aiEnhancePerDay, window: 86400, // 24 hours })), aiEnhanceHandler ); // Login: strict per-IP to prevent // brute force app.post('/api/auth/login', rateLimitMiddleware(() => ({ key: `login:${req.ip}`, limit: 10, window: 900, // 15 minutes })), loginHandler );
05
Graceful Degradation
// If Redis is down, DON'T block // all requests — fail open async function slidingWindowCheck( key: string, limit: number, windowSec: number, ): Promise<RateLimitResult> { try { // ... normal Redis logic ... } catch (error) { logger.error( 'Rate limiter Redis error', { error }); // Fail open: allow the request // Better to serve some abusers // than to block all users return { allowed: true, limit, remaining: limit, resetAt: Math.floor( Date.now() / 1000) + windowSec, }; } } // Also: in-memory fallback // If Redis is down for > 1 minute, // switch to a Map-based limiter // (per-instance, not distributed, // but better than nothing).
06
Close the Ticket
git switch -c security/PIXELCRAFT-102-rate-limiting git add src/lib/rateLimiter.ts src/middleware/ src/config/ git commit -m "Add sliding window rate limiting with Redis (PIXELCRAFT-102)" git push origin security/PIXELCRAFT-102-rate-limiting # PR → Review → Merge → Close ticket ✅
CS.DEEP-DIVE

Four rate limiting algorithms, each with different tradeoffs.

The choice depends on whether you prioritize simplicity, fairness, burst tolerance, or smooth traffic shaping.

// Algorithm comparison:

Fixed Window
  Count per calendar interval
  ✅ Simple, O(1) memory
  ❌ Boundary burst (2× at edges)

Sliding Window Log
  Store timestamp of each request
  ✅ Perfectly accurate
  ❌ O(n) memory per user

Sliding Window Counter
  Weight previous + current window
  ✅ Smooth, O(1) memory
  ✅ Good accuracy (~0.003% error)
  ← PixelCraft uses this

Token Bucket
  Tokens refill at steady rate
  ✅ Allows controlled bursts
  ✅ Used by AWS, Stripe, GitHub

Leaky Bucket
  Queue drains at constant rate
  ✅ Smoothest output
  ❌ Adds latency (queuing)

// Redis makes all of these
// distributed: works across
// multiple server instances.
"Rate Limiting Lab"
[A]Implement token bucket alongside sliding window. Compare: send 50 requests in 1 second, then 1 request/second for 60 seconds. Which algorithm handles the burst better? Which is fairer to steady users?
[B]Add client-side rate limit awareness: read X-RateLimit-Remaining from response headers. When remaining < 10, show a warning toast. When remaining = 0, disable the button and show a countdown from Retry-After.
[C]Research: how does Cloudflare's rate limiting work? How does GitHub's API rate limiting handle OAuth vs unauthenticated? What is "rate limiting as a service" and when should you use infrastructure-level limiting vs application-level?
REF.MATERIAL
ARTICLE
Cloudflare
Cloudflare's deep dive into rate limiting: fixed window, sliding window, token bucket. The math, the tradeoffs, and implementation with Redis.
RATE LIMITTHEORYESSENTIAL
VIDEO
ByteByteGo
System design perspective: all four algorithms animated, Redis implementation, distributed rate limiting across multiple servers.
SYSTEM DESIGNVISUAL
ARTICLE
Redis
Official Redis rate limiting patterns: INCR + EXPIRE, sorted sets for sliding logs, Lua scripts for atomic operations.
REDISOFFICIAL
ARTICLE
MDN Web Docs
HTTP 429 status code, Retry-After header, and rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset).
HTTPOFFICIAL
VIDEO
Abdul Bari
Clear animated explanation of the token bucket algorithm: refill rate, bucket capacity, burst handling. The algorithm behind most cloud API rate limiters.
ALGORITHMANIMATED
// LEAVE EXCITED BECAUSE
Sliding window rate limiter with Redis — no boundary bursts. Free: 100/min. Premium: 1000/min. Proper headers: X-RateLimit-Remaining tells clients their quota. 429 with Retry-After instead of a crashed server.