DEV Community

Cover image for API Rate Limiting: How to Protect Your App from Abuse
10000coders
10000coders

Posted on

API Rate Limiting: How to Protect Your App from Abuse

Introduction
API rate limiting is a crucial security and performance feature that helps protect your applications from abuse, ensures fair usage, and maintains service reliability. This guide will explore different rate limiting strategies, implementation approaches, and best practices.

What is API Rate Limiting?
API rate limiting is a technique used to control the rate of requests a client can make to an API within a specified time window. It helps prevent:

DDoS attacks
Brute force attempts
Resource exhaustion
Unfair usage
Service degradation

Rate Limiting Strategies

  1. Fixed Window Rate Limiting The simplest approach that limits requests within a fixed time window.
// Fixed Window Rate Limiter Implementation
class FixedWindowRateLimiter {
  constructor(windowSize, maxRequests) {
    this.windowSize = windowSize; // in milliseconds
    this.maxRequests = maxRequests;
    this.requests = new Map();
  }

  isAllowed(clientId) {
    const now = Date.now();
    const windowStart = Math.floor(now / this.windowSize) * this.windowSize;

    if (!this.requests.has(clientId)) {
      this.requests.set(clientId, {
        windowStart,
        count: 1
      });
      return true;
    }

    const clientData = this.requests.get(clientId);

    if (now - clientData.windowStart >= this.windowSize) {
      clientData.windowStart = windowStart;
      clientData.count = 1;
      return true;
    }

    if (clientData.count >= this.maxRequests) {
      return false;
    }

    clientData.count++;
    return true;
  }
}

// Usage Example
const rateLimiter = new FixedWindowRateLimiter(60000, 100); // 100 requests per minute
Enter fullscreen mode Exit fullscreen mode
  1. Sliding Window Rate Limiting A more sophisticated approach that provides smoother rate limiting.
// Sliding Window Rate Limiter Implementation
class SlidingWindowRateLimiter {
  constructor(windowSize, maxRequests) {
    this.windowSize = windowSize;
    this.maxRequests = maxRequests;
    this.requests = new Map();
  }

  isAllowed(clientId) {
    const now = Date.now();
    const windowStart = now - this.windowSize;

    if (!this.requests.has(clientId)) {
      this.requests.set(clientId, [now]);
      return true;
    }

    const timestamps = this.requests.get(clientId);
    const validTimestamps = timestamps.filter(time => time > windowStart);

    if (validTimestamps.length >= this.maxRequests) {
      return false;
    }

    validTimestamps.push(now);
    this.requests.set(clientId, validTimestamps);
    return true;
  }
}

// Usage Example
const rateLimiter = new SlidingWindowRateLimiter(60000, 100);
Enter fullscreen mode Exit fullscreen mode
  1. Token Bucket Rate Limiting A flexible approach that allows for burst traffic while maintaining average rate limits.
// Token Bucket Rate Limiter Implementation
class TokenBucketRateLimiter {
  constructor(capacity, refillRate) {
    this.capacity = capacity;
    this.refillRate = refillRate; // tokens per second
    this.tokens = new Map();
  }

  isAllowed(clientId) {
    const now = Date.now();

    if (!this.tokens.has(clientId)) {
      this.tokens.set(clientId, {
        tokens: this.capacity,
        lastRefill: now
      });
    }

    const clientData = this.tokens.get(clientId);
    const timePassed = (now - clientData.lastRefill) / 1000;
    const newTokens = timePassed * this.refillRate;

    clientData.tokens = Math.min(
      this.capacity,
      clientData.tokens + newTokens
    );
    clientData.lastRefill = now;

    if (clientData.tokens < 1) {
      return false;
    }

    clientData.tokens--;
    return true;
  }
}

// Usage Example
const rateLimiter = new TokenBucketRateLimiter(100, 10); // 100 tokens, 10 per second
Enter fullscreen mode Exit fullscreen mode

Implementation Approaches

  1. Express.js Middleware
const express = require('express');
const app = express();

// Rate Limiting Middleware
function rateLimit(options) {
  const limiter = new SlidingWindowRateLimiter(
    options.windowSize,
    options.maxRequests
  );

  return (req, res, next) => {
    const clientId = req.ip; // or use API key, user ID, etc.

    if (!limiter.isAllowed(clientId)) {
      return res.status(429).json({
        error: 'Too Many Requests',
        retryAfter: options.windowSize / 1000
      });
    }

    next();
  };
}

// Apply Rate Limiting
app.use('/api/', rateLimit({
  windowSize: 60000, // 1 minute
  maxRequests: 100
}));
Enter fullscreen mode Exit fullscreen mode

  1. Redis-based Distributed Rate Limiting
const Redis = require('ioredis');
const redis = new Redis();

class RedisRateLimiter {
  constructor(redis, options) {
    this.redis = redis;
    this.windowSize = options.windowSize;
    this.maxRequests = options.maxRequests;
  }

  async isAllowed(clientId) {
    const key = `ratelimit:${clientId}`;
    const now = Date.now();
    const windowStart = now - this.windowSize;

    // Remove old requests
    await this.redis.zremrangebyscore(key, 0, windowStart);

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

    if (requestCount >= this.maxRequests) {
      return false;
    }

    // Add new request
    await this.redis.zadd(key, now, `${now}`);
    await this.redis.expire(key, this.windowSize / 1000);

    return true;
  }
}

// Usage Example
const rateLimiter = new RedisRateLimiter(redis, {
  windowSize: 60000,
  maxRequests: 100
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Rate Limit Headers
function addRateLimitHeaders(res, limit, remaining, reset) {
  res.set({
    'X-RateLimit-Limit': limit,
    'X-RateLimit-Remaining': remaining,
    'X-RateLimit-Reset': reset
  });
}

// Usage in middleware
app.use('/api/', (req, res, next) => {
  const clientId = req.ip;
  const { isAllowed, limit, remaining, reset } = rateLimiter.check(clientId);

  addRateLimitHeaders(res, limit, remaining, reset);

  if (!isAllowed) {
    return res.status(429).json({
      error: 'Too Many Requests',
      retryAfter: reset
    });
  }

  next();
});
Enter fullscreen mode Exit fullscreen mode

  1. Different Limits for Different Endpoints
const rateLimits = {
  '/api/public': { windowSize: 60000, maxRequests: 100 },
  '/api/premium': { windowSize: 60000, maxRequests: 1000 },
  '/api/admin': { windowSize: 60000, maxRequests: 10000 }
};

app.use('/api/*', (req, res, next) => {
  const path = req.path;
  const limit = rateLimits[path] || rateLimits['/api/public'];
  // Apply rate limiting based on path
});
Enter fullscreen mode Exit fullscreen mode
  1. IP-based and User-based Rate Limiting
function getClientIdentifier(req) {
  // Check for API key first
  const apiKey = req.headers['x-api-key'];
  if (apiKey) {
    return `api:${apiKey}`;
  }

  // Check for authenticated user
  if (req.user) {
    return `user:${req.user.id}`;
  }

  // Fall back to IP address
  return `ip:${req.ip}`;
}
Enter fullscreen mode Exit fullscreen mode

Monitoring and Analytics

  1. Rate Limit Metrics
class RateLimitMetrics {
  constructor() {
    this.metrics = {
      totalRequests: 0,
      blockedRequests: 0,
      byClient: new Map()
    };
  }

  recordRequest(clientId, wasBlocked) {
    this.metrics.totalRequests++;
    if (wasBlocked) {
      this.metrics.blockedRequests++;
    }

    if (!this.metrics.byClient.has(clientId)) {
      this.metrics.byClient.set(clientId, {
        total: 0,
        blocked: 0
      });
    }

    const clientMetrics = this.metrics.byClient.get(clientId);
    clientMetrics.total++;
    if (wasBlocked) {
      clientMetrics.blocked++;
    }
  }

  getMetrics() {
    return {
      ...this.metrics,
      byClient: Object.fromEntries(this.metrics.byClient)
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Security Considerations
Protect Rate Limit Headers

Don't expose internal implementation details
Use standard header names
Consider security implications
Handle Edge Cases

Network failures
Clock skew
Distributed systems
Cache invalidation
Implement Graceful Degradation

Fallback mechanisms
Circuit breakers
Retry strategies
Conclusion
API rate limiting is essential for protecting your applications and ensuring fair usage. Choose the right strategy based on your needs, implement it properly, and monitor its effectiveness.

Key Takeaways
Choose the appropriate rate limiting strategy
Implement proper monitoring
Use standard headers
Consider distributed systems
Handle edge cases
Monitor and adjust limits
Document your rate limiting policy
Test thoroughly
๐Ÿš€ Ready to kickstart your tech career?
๐Ÿ‘‰ [Apply to 10000Coders]
๐ŸŽ“ [Learn Web Development for Free]
๐ŸŒŸ [See how we helped 2500+ students get jobs]

Top comments (0)