Back to Blog
APIRate LimitingSecurityPerformance

API Rate Limiting Implementation Strategies

Protect your API from abuse. From token bucket to sliding window to distributed rate limiting techniques.

B
Bootspring Team
Engineering
January 12, 2024
5 min read

Rate limiting protects your API from abuse and ensures fair usage. Here are proven algorithms and implementation patterns for production systems.

Why Rate Limit?#

Protection against: - DDoS attacks - Brute force attempts - API abuse - Resource exhaustion - Unfair usage Business benefits: - Predictable costs - SLA enforcement - Monetization tiers - Quality of service

Token Bucket Algorithm#

1class TokenBucket { 2 private tokens: number; 3 private lastRefill: number; 4 5 constructor( 6 private capacity: number, 7 private refillRate: number // tokens per second 8 ) { 9 this.tokens = capacity; 10 this.lastRefill = Date.now(); 11 } 12 13 consume(tokens: number = 1): boolean { 14 this.refill(); 15 16 if (this.tokens >= tokens) { 17 this.tokens -= tokens; 18 return true; 19 } 20 21 return false; 22 } 23 24 private refill() { 25 const now = Date.now(); 26 const elapsed = (now - this.lastRefill) / 1000; 27 const newTokens = elapsed * this.refillRate; 28 29 this.tokens = Math.min(this.capacity, this.tokens + newTokens); 30 this.lastRefill = now; 31 } 32 33 getTokens(): number { 34 this.refill(); 35 return this.tokens; 36 } 37} 38 39// Usage 40const bucket = new TokenBucket(100, 10); // 100 capacity, 10/sec refill 41 42if (bucket.consume()) { 43 // Process request 44} else { 45 // Rate limited 46}

Sliding Window Algorithm#

1class SlidingWindowRateLimiter { 2 private requests: Map<string, number[]> = new Map(); 3 4 constructor( 5 private windowMs: number, 6 private maxRequests: number 7 ) {} 8 9 isAllowed(key: string): boolean { 10 const now = Date.now(); 11 const windowStart = now - this.windowMs; 12 13 // Get existing requests for this key 14 let timestamps = this.requests.get(key) || []; 15 16 // Remove expired timestamps 17 timestamps = timestamps.filter((ts) => ts > windowStart); 18 19 if (timestamps.length >= this.maxRequests) { 20 return false; 21 } 22 23 // Add current request 24 timestamps.push(now); 25 this.requests.set(key, timestamps); 26 27 return true; 28 } 29 30 getRemainingRequests(key: string): number { 31 const now = Date.now(); 32 const windowStart = now - this.windowMs; 33 const timestamps = this.requests.get(key) || []; 34 const validRequests = timestamps.filter((ts) => ts > windowStart); 35 36 return Math.max(0, this.maxRequests - validRequests.length); 37 } 38} 39 40// 100 requests per minute 41const limiter = new SlidingWindowRateLimiter(60000, 100);

Redis-Based Distributed Rate Limiting#

1import Redis from 'ioredis'; 2 3class RedisRateLimiter { 4 constructor( 5 private redis: Redis, 6 private windowMs: number, 7 private maxRequests: number 8 ) {} 9 10 async isAllowed(key: string): Promise<{ 11 allowed: boolean; 12 remaining: number; 13 resetAt: number; 14 }> { 15 const now = Date.now(); 16 const windowKey = `ratelimit:${key}:${Math.floor(now / this.windowMs)}`; 17 18 const multi = this.redis.multi(); 19 multi.incr(windowKey); 20 multi.pexpire(windowKey, this.windowMs); 21 22 const results = await multi.exec(); 23 const count = results![0][1] as number; 24 25 const resetAt = (Math.floor(now / this.windowMs) + 1) * this.windowMs; 26 27 return { 28 allowed: count <= this.maxRequests, 29 remaining: Math.max(0, this.maxRequests - count), 30 resetAt, 31 }; 32 } 33} 34 35// Sliding window with Redis 36class RedisSlidingWindowLimiter { 37 constructor( 38 private redis: Redis, 39 private windowMs: number, 40 private maxRequests: number 41 ) {} 42 43 async isAllowed(key: string): Promise<boolean> { 44 const now = Date.now(); 45 const windowStart = now - this.windowMs; 46 const redisKey = `ratelimit:sliding:${key}`; 47 48 // Lua script for atomic operation 49 const script = ` 50 local key = KEYS[1] 51 local now = tonumber(ARGV[1]) 52 local window_start = tonumber(ARGV[2]) 53 local max_requests = tonumber(ARGV[3]) 54 local window_ms = tonumber(ARGV[4]) 55 56 -- Remove old entries 57 redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start) 58 59 -- Count current entries 60 local count = redis.call('ZCARD', key) 61 62 if count < max_requests then 63 -- Add new entry 64 redis.call('ZADD', key, now, now .. '-' .. math.random()) 65 redis.call('PEXPIRE', key, window_ms) 66 return 1 67 else 68 return 0 69 end 70 `; 71 72 const result = await this.redis.eval( 73 script, 74 1, 75 redisKey, 76 now, 77 windowStart, 78 this.maxRequests, 79 this.windowMs 80 ); 81 82 return result === 1; 83 } 84}

Express Middleware#

1import rateLimit from 'express-rate-limit'; 2import RedisStore from 'rate-limit-redis'; 3 4// Basic in-memory rate limiting 5const basicLimiter = rateLimit({ 6 windowMs: 15 * 60 * 1000, // 15 minutes 7 max: 100, // 100 requests per window 8 message: { 9 error: 'Too many requests', 10 retryAfter: 900, 11 }, 12 standardHeaders: true, // Return rate limit info in headers 13 legacyHeaders: false, 14}); 15 16// Redis-backed for distributed systems 17const redisLimiter = rateLimit({ 18 windowMs: 15 * 60 * 1000, 19 max: 100, 20 store: new RedisStore({ 21 sendCommand: (...args: string[]) => redis.call(...args), 22 }), 23}); 24 25// Different limits for different routes 26const authLimiter = rateLimit({ 27 windowMs: 60 * 60 * 1000, // 1 hour 28 max: 5, // 5 login attempts per hour 29 skipSuccessfulRequests: true, 30}); 31 32// Apply to routes 33app.use('/api/', basicLimiter); 34app.use('/api/auth/login', authLimiter);

Response Headers#

1function addRateLimitHeaders( 2 res: Response, 3 limit: number, 4 remaining: number, 5 resetAt: number 6) { 7 res.setHeader('X-RateLimit-Limit', limit); 8 res.setHeader('X-RateLimit-Remaining', remaining); 9 res.setHeader('X-RateLimit-Reset', Math.ceil(resetAt / 1000)); 10 11 // Standard headers (RFC 6585) 12 res.setHeader('RateLimit-Limit', limit); 13 res.setHeader('RateLimit-Remaining', remaining); 14 res.setHeader('RateLimit-Reset', Math.ceil(resetAt / 1000)); 15} 16 17// When rate limited 18function sendRateLimitResponse(res: Response, resetAt: number) { 19 res.status(429); 20 res.setHeader('Retry-After', Math.ceil((resetAt - Date.now()) / 1000)); 21 res.json({ 22 error: 'Too Many Requests', 23 message: 'Rate limit exceeded. Please try again later.', 24 retryAfter: Math.ceil((resetAt - Date.now()) / 1000), 25 }); 26}

Tiered Rate Limiting#

1interface RateLimitTier { 2 requestsPerMinute: number; 3 requestsPerDay: number; 4 burstLimit: number; 5} 6 7const tiers: Record<string, RateLimitTier> = { 8 free: { 9 requestsPerMinute: 10, 10 requestsPerDay: 1000, 11 burstLimit: 20, 12 }, 13 pro: { 14 requestsPerMinute: 100, 15 requestsPerDay: 50000, 16 burstLimit: 200, 17 }, 18 enterprise: { 19 requestsPerMinute: 1000, 20 requestsPerDay: 1000000, 21 burstLimit: 2000, 22 }, 23}; 24 25class TieredRateLimiter { 26 async checkLimit(userId: string, tier: string): Promise<boolean> { 27 const limits = tiers[tier] || tiers.free; 28 29 // Check minute limit 30 const minuteKey = `ratelimit:minute:${userId}`; 31 const minuteCount = await this.redis.incr(minuteKey); 32 if (minuteCount === 1) { 33 await this.redis.expire(minuteKey, 60); 34 } 35 36 if (minuteCount > limits.requestsPerMinute) { 37 return false; 38 } 39 40 // Check daily limit 41 const dayKey = `ratelimit:day:${userId}:${this.getDay()}`; 42 const dayCount = await this.redis.incr(dayKey); 43 if (dayCount === 1) { 44 await this.redis.expire(dayKey, 86400); 45 } 46 47 if (dayCount > limits.requestsPerDay) { 48 return false; 49 } 50 51 return true; 52 } 53 54 private getDay(): string { 55 return new Date().toISOString().split('T')[0]; 56 } 57}

Best Practices#

DO: ✓ Use distributed storage for multi-instance ✓ Return informative headers ✓ Implement graceful degradation ✓ Different limits for different endpoints ✓ Consider user tiers ✓ Log rate limit events DON'T: ✗ Rate limit health checks ✗ Use only IP-based limiting ✗ Set limits too low for normal usage ✗ Forget to handle edge cases ✗ Ignore legitimate high-volume users

Conclusion#

Rate limiting is essential API protection. Choose the right algorithm for your use case—token bucket for bursts, sliding window for smooth limits—and use distributed storage for scaled systems.

Always communicate limits clearly through headers and error messages.

Share this article

Help spread the word about Bootspring