# Rate Limiting Server: Throttle API Requests

# 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)

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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.
