Skip to main content

Command Palette

Search for a command to run...

Rate Limiting Server: Throttle API Requests

Learn: Rate Limiting Server: Throttle API Requests

Updated
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Rate Limiting Server: Throttle API Requests

Problem

APIs are vulnerable to abuse through excessive requests that can:

  • Exhaust server resources and cause denial-of-service (DoS)
  • Degrade performance for legitimate users
  • Increase infrastructure costs
  • Enable credential stuffing, brute force attacks, and scraping
  • Violate service terms and SLAs

Without rate limiting, a single malicious actor can overwhelm your entire system.

Solution

Implement a multi-layered rate limiting strategy:

  1. Token Bucket Algorithm: Allows burst traffic while maintaining average rate limits
  2. Sliding Window Counters: Tracks requests in real-time windows
  3. Distributed Rate Limiting: Uses Redis for consistency across multiple servers
  4. Tiered Limits: Different limits for different user tiers (free, premium, enterprise)
  5. Adaptive Throttling: Adjusts limits based on system load
  6. Graceful Degradation: Returns informative error responses with retry information

Code Implementation

1. Basic Token Bucket Rate Limiter (Node.js/Express)

// rateLimiter.js
class TokenBucket {
  constructor(capacity, refillRate) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillRate = refillRate; // tokens per second
    this.lastRefillTime = Date.now();
  }

  refill() {
    const now = Date.now();
    const timePassed = (now - this.lastRefillTime) / 1000;
    const tokensToAdd = timePassed * this.refillRate;

    this.tokens = Math.min(
      this.capacity,
      this.tokens + tokensToAdd
    );
    this.lastRefillTime = now;
  }

  tryConsume(tokens = 1) {
    this.refill();

    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }

  getTokensRemaining() {
    this.refill();
    return Math.floor(this.tokens);
  }
}

module.exports = TokenBucket;

2. Redis-Based Distributed Rate Limiter

// redisRateLimiter.js
const redis = require('redis');

class RedisRateLimiter {
  constructor(redisClient, options = {}) {
    this.client = redisClient;
    this.defaultLimit = options.limit || 100;
    this.defaultWindow = options.window || 60; // seconds
  }

  async isAllowed(identifier, limit = this.defaultLimit, window = this.defaultWindow) {
    const key = `rate_limit:${identifier}`;
    const now = Date.now();
    const windowStart = now - (window * 1000);

    try {
      // Remove old entries outside the window
      await this.client.zremrangebyscore(key, 0, windowStart);

      // Count requests in current window
      const requestCount = await this.client.zcard(key);

      if (requestCount < limit) {
        // Add current request
        await this.client.zadd(key, now, `${now}-${Math.random()}`);
        // Set expiration
        await this.client.expire(key, window + 1);
        return {
          allowed: true,
          remaining: limit - requestCount - 1,
          resetTime: Math.ceil((windowStart + window * 1000) / 1000)
        };
      }

      // Get oldest request time for retry-after
      const oldest = await this.client.zrange(key, 0, 0, { WITHSCORES: true });
      const retryAfter = oldest.length > 0 
        ? Math.ceil((oldest[0].score + window * 1000 - now) / 1000)
        : window;

      return {
        allowed: false,
        remaining: 0,
        retryAfter,
        resetTime: Math.ceil((oldest[0].score + window * 1000) / 1000)
      };
    } catch (error) {
      console.error('Rate limiter error:', error);
      // Fail open - allow request if Redis fails
      return { allowed: true, remaining: limit };
    }
  }
}

module.exports = RedisRateLimiter;

3. Express Middleware Implementation

// middleware/rateLimitMiddleware.js
const RedisRateLimiter = require('../redisRateLimiter');
const redis = require('redis');

const redisClient = redis.createClient({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379
});

const limiter = new RedisRateLimiter(redisClient, {
  limit: 100,
  window: 60
});

// User tier configurations
const tierLimits = {
  free: { limit: 10, window: 60 },
  premium: { limit: 100, window: 60 },
  enterprise: { limit: 10000, window: 60 }
};

const rateLimitMiddleware = async (req, res, next) => {
  try {
    // Identify user - use IP for anonymous, user ID for authenticated
    const identifier = req.user?.id || req.ip;
    const userTier = req.user?.tier || 'free';
    const { limit, window } = tierLimits[userTier];

    const result = await limiter.isAllowed(identifier, limit, window);

    // Set rate limit headers
    res.set({
      'X-RateLimit-Limit': limit,
      'X-RateLimit-Remaining': Math.max(0, result.remaining),
      'X-RateLimit-Reset': result.resetTime
    });

    if (!result.allowed) {
      res.set('Retry-After', result.retryAfter);
      return res.status(429).json({
        error: 'Too Many Requests',
        message: `Rate limit exceeded. Retry after ${result.retryAfter} seconds.`,
        retryAfter: result.retryAfter,
        resetTime: new Date(result.resetTime * 1000).toISOString()
      });
    }

    next();
  } catch (error) {
    console.error('Rate limit middleware error:', error);
    next(); // Fail open
  }
};

module.exports = rateLimitMiddleware;

4. Endpoint-Specific Rate Limiting

// middleware/endpointRateLimits.js
const RedisRateLimiter = require('../redisRateLimiter');
const redis = require('redis');

const redisClient = redis.createClient();
const limiter = new RedisRateLimiter(redisClient);

// Endpoint-specific configurations
const endpointLimits = {
  '/api/auth/login': { limit: 5, window: 300 }, // 5 per 5 minutes
  '/api/auth/register': { limit: 3, window: 3600 }, // 3 per hour
  '/api/search': { limit: 30, window: 60 }, // 30 per minute
  '/api/export': { limit: 10, window: 3600 }, // 10 per hour
  '/api/upload': { limit: 5, window: 3600 } // 5 per hour
};

const endpointRateLimitMiddleware = async (req, res, next) => {
  const endpoint = req.route?.path || req.path;
  const config = endpointLimits[endpoint];

  if (!config) {
    return next();
  }

  const identifier = `${req.user?.id || req.ip}:${endpoint}`;
  const result = await limiter.isAllowed(
    identifier,
    config.limit,
    config.window
  );

  res.set({
    'X-RateLimit-Limit': config.limit,
    'X-RateLimit-Remaining': Math.max(0, result.remaining),
    'X-RateLimit-Reset': result.resetTime
  });

  if (!result.allowed) {
    res.set('Retry-After', result.retryAfter);
    return res.status(429).json({
      error: 'Too Many Requests',
      message: `Too many requests to ${endpoint}`,
      retryAfter: result.retryAfter
    });
  }

  next();
};

module.exports = endpointRateLimitMiddleware;

5. Express Server Setup

// server.js
const express = require('express');
const rateLimitMiddleware = require('./middleware/rateLimitMiddleware');
const endpointRateLimitMiddleware = require('./middleware/endpointRateLimits');

const app = express();

// Global rate limiting
app.use(rateLimitMiddleware);

// Endpoint-specific rate limiting
app.use(endpointRateLimitMiddleware);

// Routes
app.post('/api/auth/login', (req, res) => {
  res.json({ message: 'Login successful' });
});

app.post('/api/auth/register', (req, res) => {
  res.json({ message: 'Registration successful' });
});

app.get('/api/search', (req, res) => {
  res.json({ results: [] });
});

app.get('/api/export', (req, res) => {
  res.json({ data: 'exported' });
});

app.post('/api/upload', (req, res) => {
  res.json({ message: 'Upload successful' });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal Server Error' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

6. Advanced: Sliding Window with Quota Management

// advancedRateLimiter.js
class AdvancedRateLimiter {
  constructor(redisClient) {
    this.client = redisClient;
  }

  async checkRateLimit(userId, endpoint, config) {
    const key = `quota:${userId}:${endpoint}`;
    const now = Date.now();
    const windowStart = now - (config.window * 1000);

    // Lua script for atomic operation
    const script = `
      local key = KEYS[1]
      local now = tonumber(ARGV[1])
      local window_start = tonumber(ARGV[2])
      local limit = tonumber(ARGV[3])
      local window = tonumber(ARGV[4])

      redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
      local count = redis.call('ZCARD', key)

      if count < limit then
        redis.call('ZADD', key, now, now .. '-' .. math.random())
        redis.call('EXPIRE', key, window + 1)
        return {1, limit - count - 1, window_start + window * 1000}
      else
        local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
        local retry_after = math.ceil((oldest[2] + window * 1000 - now) / 1000)
        return {0, 0, retry_after}
      end
    `;

    try {
      const result = await this.client.eval(
        script,
        1,
        key,
        now,
        windowStart,
        config.limit,
        config.window
      );

      return {
        allowed: result[0] === 1,
        remaining: result[1],
        resetTime: result[2]
      };
    } catch (error) {
      console.error('Rate limit check failed:', error);
      return { allowed: true, remaining: config.limit };
    }
  }

  async getQuotaStatus(userId) {
    const pattern = `quota:${userId}:*`;
    const keys = await this.client.keys(pattern);

    const status = {};
    for (const key of keys) {
      const endpoint = key.split(':')[2];
      const count = await this.client.zcard(key);
      status[endpoint] = count;
    }

    return status;
  }

  async resetQuota(userId, endpoint) {
    const key = `quota:${userId}:${endpoint}`;
    await this.client.del(key);
    return { message: 'Quota reset' };
  }
}

module.exports = AdvancedRateLimiter;

7. Client-Side Retry Logic

// client/apiClient.js
class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.maxRetries = 3;
  }

  async request(method, endpoint, data = null) {
    let retries = 0;

    while (retries < this.maxRetries) {
      try {
        const response = await fetch(`${this.baseURL}${endpoint}`, {
          method,
          headers: { 'Content-Type': 'application/json' },
          body: data ? JSON.stringify(data) : null
        });

        // Handle rate limiting
        if (response.status === 429) {
          const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
          console.warn(`Rate limited. Retrying after ${retryAfter}s`);

          await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
          retries++;
          continue;
        }

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        return await response.json();
      } catch (error) {
        console.error(`Request failed: ${error.message}`);
        throw error;
      }
    }

    throw new Error('Max retries exceeded');
  }

  get(endpoint) {
    return this.request('GET', endpoint);
  }

  post(endpoint, data) {
    return this.request('POST', endpoint, data);
  }
}

// Usage
const client = new APIClient('https://api.example.com');
client.post('/api/auth/login', { email: 'user@example.com', password: '***' })
  .then(response => console.log('Login successful:', response))
  .catch(error => console.error('Login failed:', error));

Key Features

Token Bucket Algorithm - Smooth burst handling
Redis Integration - Distributed, scalable rate limiting
Tiered Limits - Different rates for user tiers
Endpoint-Specific Rules - Granular control
Informative Headers - RFC 6585 compliant responses
Retry-After Support - Client guidance
Fail-Open Design - Graceful degradation
Atomic Operations - Lua scripts prevent race conditions
Client Retry Logic - Automatic backoff

This implementation prevents abuse while maintaining excellent user experience for legitimate traffic.

Rate Limiting Server: Throttle API Requests