PureTools

Rate Limiting: Protect Your API from Abuse

PureTools Team· 7 min read
Rate Limiting: Protect Your API from Abuse

Rate Limiting: Your API's First Line of Defense

Without rate limiting, a single user (or bot) can overwhelm your API with thousands of requests per second. Rate limiting caps the number of requests a client can make in a time window.

Common Algorithms

AlgorithmHow It WorksProsCons
Fixed WindowCount requests per fixed time window (e.g., per minute)Simple to implementBurst at window boundaries
Sliding WindowCount requests in a rolling time windowSmooth distributionMore memory
Token BucketTokens refill at a fixed rate; each request costs a tokenAllows controlled burstsSlightly complex
Leaky BucketRequests queue and process at a fixed rateSmooth output rateDrops requests when full

Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100           # max requests per window
X-RateLimit-Remaining: 42        # requests left
X-RateLimit-Reset: 1682345700    # when the window resets (Unix)
Retry-After: 30                  # seconds to wait (on 429)
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
  "error": "Rate limit exceeded",
  "retry_after": 30
}

Express.js with Redis

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

const limiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute
  max: 100,             // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args),
  }),
  message: {
    error: 'Too many requests, please try again later',
  },
});

app.use('/api/', limiter);

// Different limits for different endpoints
const strictLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 10,  // only 10 requests per minute
});

app.post('/api/auth/login', strictLimiter, loginHandler);

DIY Sliding Window with Redis

async function checkRateLimit(userId, limit = 100, window = 60) {
  const key = `rate:${userId}`;
  const now = Date.now();
  const windowStart = now - window * 1000;

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart); // remove old entries
  pipeline.zadd(key, now, `${now}:${Math.random()}`); // add current
  pipeline.zcard(key); // count entries in window
  pipeline.expire(key, window); // auto-cleanup

  const results = await pipeline.exec();
  const count = results[2][1];

  return {
    allowed: count <= limit,
    remaining: Math.max(0, limit - count),
    reset: Math.ceil(windowStart / 1000) + window,
  };
}

Rate Limiting by Key

Key ByUse Case
IP addressAnonymous endpoints, public APIs
API keyAuthenticated APIs with API keys
User IDAuthenticated users
IP + endpointPer-route limits for unauthenticated users

Best Practices

  • Return 429 status code with Retry-After header
  • Include rate limit headers in every response (not just 429)
  • Use different limits for different endpoints (login vs read)
  • Log rate limit hits for abuse detection
  • Consider geographic or plan-based limits