Solidis LogoSolidis
← Back to Tutorials
Intermediate
20 min

Rate Limiting with Redis

Protect your APIs from abuse and ensure fair resource allocation using Redis-based rate limiting strategies.

What You'll Learn
  • Fixed window rate limiting
  • Sliding window rate limiting
  • Token bucket algorithm
  • Express middleware integration
1
Fixed Window Rate Limiter
Simple and efficient rate limiting strategy
1import { SolidisFeaturedClient } from '@vcms-io/solidis/featured';
2
3export class FixedWindowRateLimiter {
4  private client: SolidisFeaturedClient;
5  private prefix: string;
6
7  constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
8    this.client = new SolidisFeaturedClient({
9      host: options.host || '127.0.0.1',
10      port: options.port || 6379,
11    });
12    this.prefix = options.prefix || 'rate:';
13  }
14
15  async connect(): Promise<void> {
16    await this.client.connect();
17  }
18
19  /**
20   * Check if request is allowed
21   * @param key - Identifier (e.g., user ID, IP address)
22   * @param limit - Maximum requests allowed
23   * @param windowSeconds - Time window in seconds
24   */
25  async checkLimit(
26    key: string,
27    limit: number,
28    windowSeconds: number
29  ): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
30    const now = Date.now();
31    const windowStart = Math.floor(now / (windowSeconds * 1000));
32    const cacheKey = `${this.prefix}${key}:${windowStart}`;
33
34    // Increment counter
35    const count = await this.client.incr(cacheKey);
36
37    // Set expiry on first request
38    if (count === 1) {
39      await this.client.expire(cacheKey, windowSeconds);
40    }
41
42    const allowed = count <= limit;
43    const remaining = Math.max(0, limit - count);
44    const resetAt = new Date((windowStart + 1) * windowSeconds * 1000);
45
46    return { allowed, remaining, resetAt };
47  }
48}
2
Sliding Window Rate Limiter
More accurate rate limiting with smooth distribution
1export class SlidingWindowRateLimiter {
2  private client: SolidisFeaturedClient;
3  private prefix: string;
4
5  constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
6    this.client = new SolidisFeaturedClient({
7      host: options.host || '127.0.0.1',
8      port: options.port || 6379,
9    });
10    this.prefix = options.prefix || 'rate:sliding:';
11  }
12
13  async connect(): Promise<void> {
14    await this.client.connect();
15  }
16
17  /**
18   * Check if request is allowed using sliding window
19   */
20  async checkLimit(
21    key: string,
22    limit: number,
23    windowSeconds: number
24  ): Promise<{ allowed: boolean; remaining: number; retryAfter: number }> {
25    const cacheKey = `${this.prefix}${key}`;
26    const now = Date.now();
27    const windowStart = now - windowSeconds * 1000;
28
29    // Remove old entries
30    await this.client.zremrangebyscore(cacheKey, 0, windowStart);
31
32    // Count requests in window
33    const count = await this.client.zcard(cacheKey);
34
35    const allowed = count < limit;
36
37    if (allowed) {
38      // Add current request
39      await this.client.zadd(cacheKey, now, `${now}`);
40      // Set expiry
41      await this.client.expire(cacheKey, windowSeconds);
42    }
43
44    const remaining = Math.max(0, limit - count - (allowed ? 1 : 0));
45
46    // Calculate retry after (when oldest request will expire)
47    let retryAfter = 0;
48    if (!allowed && count > 0) {
49      const oldest = await this.client.zrange(cacheKey, 0, 0, { WITHSCORES: true });
50      if (oldest.length > 0) {
51        const oldestScore = Number.parseInt(oldest[1].toString());
52        retryAfter = Math.ceil((oldestScore + windowSeconds * 1000 - now) / 1000);
53      }
54    }
55
56    return { allowed, remaining, retryAfter };
57  }
58}
3
Token Bucket Rate Limiter
Allow burst traffic while maintaining average rate
1export class TokenBucketRateLimiter {
2  private client: SolidisFeaturedClient;
3  private prefix: string;
4
5  constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
6    this.client = new SolidisFeaturedClient({
7      host: options.host || '127.0.0.1',
8      port: options.port || 6379,
9    });
10    this.prefix = options.prefix || 'rate:bucket:';
11  }
12
13  async connect(): Promise<void> {
14    await this.client.connect();
15  }
16
17  /**
18   * Check if request is allowed using token bucket
19   * @param capacity - Maximum tokens in bucket
20   * @param refillRate - Tokens added per second
21   */
22  async checkLimit(
23    key: string,
24    capacity: number,
25    refillRate: number
26  ): Promise<{ allowed: boolean; tokens: number; retryAfter: number }> {
27    const cacheKey = `${this.prefix}${key}`;
28    const now = Date.now();
29
30    // Get bucket state
31    const result = await this.client.hmget(cacheKey, 'tokens', 'lastRefill');
32    let tokens = result[0] ? Number.parseFloat(result[0].toString()) : capacity;
33    let lastRefill = result[1] ? Number.parseInt(result[1].toString()) : now;
34
35    // Calculate tokens to add
36    const timePassed = (now - lastRefill) / 1000;
37    const tokensToAdd = timePassed * refillRate;
38    tokens = Math.min(capacity, tokens + tokensToAdd);
39
40    const allowed = tokens >= 1;
41
42    if (allowed) {
43      tokens -= 1;
44    }
45
46    // Update bucket state
47    await this.client.hset(cacheKey, {
48      tokens: tokens.toString(),
49      lastRefill: now.toString(),
50    });
51    await this.client.expire(cacheKey, Math.ceil(capacity / refillRate) + 60);
52
53    const retryAfter = allowed ? 0 : Math.ceil((1 - tokens) / refillRate);
54
55    return {
56      allowed,
57      tokens: Math.floor(tokens),
58      retryAfter,
59    };
60  }
61}
4
Express Middleware Integration
Easy-to-use middleware for your Express application
1import { Request, Response, NextFunction } from 'express';
2import { SlidingWindowRateLimiter } from './sliding-window-limiter';
3
4export interface RateLimitOptions {
5  windowSeconds?: number;
6  limit?: number;
7  keyGenerator?: (req: Request) => string;
8  skip?: (req: Request) => boolean;
9  handler?: (req: Request, res: Response) => void;
10}
11
12export function createRateLimiter(
13  limiter: SlidingWindowRateLimiter,
14  options: RateLimitOptions = {}
15) {
16  const {
17    windowSeconds = 60,
18    limit = 100,
19    keyGenerator = (req) => req.ip || 'unknown',
20    skip = () => false,
21    handler = (req, res) => {
22      res.status(429).json({
23        error: 'Too many requests',
24        message: 'Please try again later',
25      });
26    },
27  } = options;
28
29  return async (req: Request, res: Response, next: NextFunction) => {
30    // Skip rate limiting if condition is met
31    if (skip(req)) {
32      return next();
33    }
34
35    const key = keyGenerator(req);
36
37    try {
38      const result = await limiter.checkLimit(key, limit, windowSeconds);
39
40      // Set rate limit headers
41      res.setHeader('X-RateLimit-Limit', limit.toString());
42      res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
43
44      if (!result.allowed) {
45        res.setHeader('Retry-After', result.retryAfter.toString());
46        return handler(req, res);
47      }
48
49      next();
50    } catch (error) {
51      console.error('Rate limiter error:', error);
52      // Fail open - allow request on error
53      next();
54    }
55  };
56}
57
58// Usage examples
59export function createUserRateLimiter(limiter: SlidingWindowRateLimiter) {
60  return createRateLimiter(limiter, {
61    windowSeconds: 60,
62    limit: 100,
63    keyGenerator: (req) => `user:${req.user?.id || req.ip}`,
64    skip: (req) => req.user?.role === 'admin',
65  });
66}
67
68export function createAPIRateLimiter(limiter: SlidingWindowRateLimiter) {
69  return createRateLimiter(limiter, {
70    windowSeconds: 3600, // 1 hour
71    limit: 1000,
72    keyGenerator: (req) => {
73      const apiKey = req.headers['x-api-key'] as string;
74      return `api:${apiKey || req.ip}`;
75    },
76  });
77}
78
79export function createLoginRateLimiter(limiter: SlidingWindowRateLimiter) {
80  return createRateLimiter(limiter, {
81    windowSeconds: 900, // 15 minutes
82    limit: 5,
83    keyGenerator: (req) => `login:${req.ip}`,
84    handler: (req, res) => {
85      res.status(429).json({
86        error: 'Too many login attempts',
87        message: 'Please try again in 15 minutes',
88      });
89    },
90  });
91}
Complete Usage Example
1import express from 'express';
2import { SlidingWindowRateLimiter } from './sliding-window-limiter';
3import {
4  createUserRateLimiter,
5  createAPIRateLimiter,
6  createLoginRateLimiter,
7} from './rate-limit-middleware';
8
9const app = express();
10const limiter = new SlidingWindowRateLimiter({
11  host: '127.0.0.1',
12  port: 6379,
13});
14
15app.use(express.json());
16
17// Global rate limiter
18app.use(createUserRateLimiter(limiter));
19
20// Stricter rate limit for login
21app.post('/api/login', createLoginRateLimiter(limiter), async (req, res) => {
22  // Login logic
23  res.json({ message: 'Login successful' });
24});
25
26// API key based rate limiting
27app.use('/api/v1', createAPIRateLimiter(limiter));
28
29app.get('/api/v1/data', (req, res) => {
30  res.json({ data: 'sensitive data' });
31});
32
33// Start server
34async function start() {
35  await limiter.connect();
36  app.listen(3000, () => {
37    console.log('Server running on http://localhost:3000');
38  });
39}
40
41start().catch(console.error);
Best Practices
  • Choose the right algorithm
    Fixed window for simplicity, sliding window for accuracy, token bucket for burst traffic
  • Always include rate limit headers
    Help clients understand and respect rate limits
  • Fail open on errors
    Don't block all traffic if Redis is down
  • Use different limits for different endpoints
    More restrictive for expensive operations