Back to Blog
APIRate LimitingSecurityBackend

API Rate Limiting: Protecting Your Services

Implement effective API rate limiting. Learn algorithms, patterns, and strategies for protecting your services from abuse.

B
Bootspring Team
Engineering
February 26, 2026
5 min read

Rate limiting protects your API from abuse and ensures fair usage. This guide covers algorithms, implementation patterns, and best practices.

Why Rate Limiting?#

  • Prevent abuse: Stop malicious actors from overwhelming your service
  • Ensure fairness: Distribute resources fairly among users
  • Protect infrastructure: Prevent cascading failures
  • Cost control: Limit expensive operations

Rate Limiting Algorithms#

Fixed Window#

Simple but has burst issues at window boundaries:

1class FixedWindowRateLimiter { 2 private windows: Map<string, { count: number; resetAt: number }> = new Map(); 3 4 constructor(private limit: number, private windowMs: number) {} 5 6 isAllowed(key: string): boolean { 7 const now = Date.now(); 8 const window = this.windows.get(key); 9 10 if (!window || now >= window.resetAt) { 11 this.windows.set(key, { 12 count: 1, 13 resetAt: now + this.windowMs, 14 }); 15 return true; 16 } 17 18 if (window.count < this.limit) { 19 window.count++; 20 return true; 21 } 22 23 return false; 24 } 25}

Sliding Window Log#

More accurate but memory-intensive:

1class SlidingWindowLogRateLimiter { 2 private requests: Map<string, number[]> = new Map(); 3 4 constructor(private limit: number, private windowMs: number) {} 5 6 isAllowed(key: string): boolean { 7 const now = Date.now(); 8 const windowStart = now - this.windowMs; 9 10 let timestamps = this.requests.get(key) || []; 11 timestamps = timestamps.filter(t => t > windowStart); 12 13 if (timestamps.length < this.limit) { 14 timestamps.push(now); 15 this.requests.set(key, timestamps); 16 return true; 17 } 18 19 return false; 20 } 21}

Sliding Window Counter#

Hybrid approach with better memory efficiency:

1class SlidingWindowCounterRateLimiter { 2 private windows: Map<string, { 3 current: number; 4 previous: number; 5 currentStart: number; 6 }> = new Map(); 7 8 constructor(private limit: number, private windowMs: number) {} 9 10 isAllowed(key: string): boolean { 11 const now = Date.now(); 12 const currentWindow = Math.floor(now / this.windowMs); 13 14 let data = this.windows.get(key); 15 16 if (!data || data.currentStart < currentWindow - 1) { 17 data = { current: 0, previous: 0, currentStart: currentWindow }; 18 this.windows.set(key, data); 19 } else if (data.currentStart < currentWindow) { 20 data.previous = data.current; 21 data.current = 0; 22 data.currentStart = currentWindow; 23 } 24 25 // Calculate weighted count 26 const elapsed = (now % this.windowMs) / this.windowMs; 27 const weightedCount = data.previous * (1 - elapsed) + data.current; 28 29 if (weightedCount < this.limit) { 30 data.current++; 31 return true; 32 } 33 34 return false; 35 } 36}

Token Bucket#

Allows bursts while maintaining average rate:

1class TokenBucketRateLimiter { 2 private buckets: Map<string, { tokens: number; lastRefill: number }> = new Map(); 3 4 constructor( 5 private capacity: number, 6 private refillRate: number, // tokens per second 7 ) {} 8 9 isAllowed(key: string, tokens: number = 1): boolean { 10 const now = Date.now(); 11 let bucket = this.buckets.get(key); 12 13 if (!bucket) { 14 bucket = { tokens: this.capacity, lastRefill: now }; 15 this.buckets.set(key, bucket); 16 } 17 18 // Refill tokens 19 const elapsed = (now - bucket.lastRefill) / 1000; 20 bucket.tokens = Math.min( 21 this.capacity, 22 bucket.tokens + elapsed * this.refillRate 23 ); 24 bucket.lastRefill = now; 25 26 if (bucket.tokens >= tokens) { 27 bucket.tokens -= tokens; 28 return true; 29 } 30 31 return false; 32 } 33}

Redis Implementation#

Distributed rate limiting with Redis:

1import Redis from 'ioredis'; 2 3class RedisRateLimiter { 4 constructor( 5 private redis: Redis, 6 private limit: number, 7 private windowMs: number 8 ) {} 9 10 async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number; resetAt: number }> { 11 const now = Date.now(); 12 const windowKey = `ratelimit:${key}:${Math.floor(now / this.windowMs)}`; 13 14 const multi = this.redis.multi(); 15 multi.incr(windowKey); 16 multi.pexpire(windowKey, this.windowMs); 17 18 const results = await multi.exec(); 19 const count = results?.[0]?.[1] as number; 20 21 const allowed = count <= this.limit; 22 const remaining = Math.max(0, this.limit - count); 23 const resetAt = (Math.floor(now / this.windowMs) + 1) * this.windowMs; 24 25 return { allowed, remaining, resetAt }; 26 } 27} 28 29// Sliding window with Redis sorted sets 30class RedisSlidingWindowLimiter { 31 constructor( 32 private redis: Redis, 33 private limit: number, 34 private windowMs: number 35 ) {} 36 37 async isAllowed(key: string): Promise<boolean> { 38 const now = Date.now(); 39 const windowStart = now - this.windowMs; 40 const setKey = `ratelimit:sliding:${key}`; 41 42 await this.redis.zremrangebyscore(setKey, 0, windowStart); 43 44 const count = await this.redis.zcard(setKey); 45 46 if (count < this.limit) { 47 await this.redis 48 .multi() 49 .zadd(setKey, now.toString(), `${now}-${Math.random()}`) 50 .pexpire(setKey, this.windowMs) 51 .exec(); 52 return true; 53 } 54 55 return false; 56 } 57}

Express Middleware#

1import { Request, Response, NextFunction } from 'express'; 2 3function rateLimitMiddleware(limiter: RateLimiter) { 4 return async (req: Request, res: Response, next: NextFunction) => { 5 const key = req.ip || req.headers['x-forwarded-for'] as string; 6 7 const result = await limiter.isAllowed(key); 8 9 // Set rate limit headers 10 res.set({ 11 'X-RateLimit-Limit': limiter.limit.toString(), 12 'X-RateLimit-Remaining': result.remaining.toString(), 13 'X-RateLimit-Reset': result.resetAt.toString(), 14 }); 15 16 if (!result.allowed) { 17 res.status(429).json({ 18 error: 'Too Many Requests', 19 retryAfter: Math.ceil((result.resetAt - Date.now()) / 1000), 20 }); 21 return; 22 } 23 24 next(); 25 }; 26} 27 28// Usage 29app.use('/api', rateLimitMiddleware( 30 new RedisRateLimiter(redis, 100, 60000) // 100 requests per minute 31));

Tiered Rate Limits#

Different limits for different users:

1interface RateLimitTier { 2 name: string; 3 requestsPerMinute: number; 4 requestsPerDay: number; 5} 6 7const tiers: Record<string, RateLimitTier> = { 8 free: { name: 'Free', requestsPerMinute: 10, requestsPerDay: 1000 }, 9 pro: { name: 'Pro', requestsPerMinute: 100, requestsPerDay: 50000 }, 10 enterprise: { name: 'Enterprise', requestsPerMinute: 1000, requestsPerDay: 1000000 }, 11}; 12 13async function getTierForUser(userId: string): Promise<RateLimitTier> { 14 const user = await db.users.findById(userId); 15 return tiers[user.plan] || tiers.free; 16} 17 18function tieredRateLimitMiddleware() { 19 return async (req: Request, res: Response, next: NextFunction) => { 20 const userId = req.user?.id; 21 const tier = await getTierForUser(userId); 22 23 const minuteKey = `${userId}:minute`; 24 const dayKey = `${userId}:day`; 25 26 const [minuteAllowed, dayAllowed] = await Promise.all([ 27 limiter.isAllowed(minuteKey, tier.requestsPerMinute, 60000), 28 limiter.isAllowed(dayKey, tier.requestsPerDay, 86400000), 29 ]); 30 31 if (!minuteAllowed.allowed || !dayAllowed.allowed) { 32 return res.status(429).json({ error: 'Rate limit exceeded' }); 33 } 34 35 next(); 36 }; 37}

Best Practices#

  1. Clear error messages: Tell users when they can retry
  2. Rate limit headers: Include X-RateLimit-* headers
  3. Gradual backoff: Increase limits for good actors
  4. Multiple dimensions: Limit by IP, user, and endpoint
  5. Monitoring: Alert on unusual patterns

Conclusion#

Choose the right algorithm based on your needs: token bucket for APIs allowing bursts, sliding window for strict limits. Implement at multiple layers and provide clear feedback to API consumers.

Share this article

Help spread the word about Bootspring