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#
- Clear error messages: Tell users when they can retry
- Rate limit headers: Include X-RateLimit-* headers
- Gradual backoff: Increase limits for good actors
- Multiple dimensions: Limit by IP, user, and endpoint
- 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.