正在加载,请稍候…

What Is Rate Limiting? Algorithms, Implementation, and Best Practices

Understand rate limiting — why it matters, the main algorithms (token bucket, sliding window), how to implement it in Express, and standard rate limit HTTP headers.

What Is Rate Limiting?

Rate limiting controls how many requests a client can make to an API within a time window. Without it, a single misbehaving client can flood your server, degrading service for everyone else.

Why rate limiting matters:

  • Protects against DoS/DDoS attacks
  • Prevents API abuse and scraping
  • Ensures fair resource distribution among clients
  • Reduces infrastructure costs
  • Catches accidental bugs (tight loops hitting your API)

Rate Limiting Algorithms

1. Fixed Window Counter

Count requests in a fixed time window (e.g., each 60-second minute starting at :00).

async function isRateLimited(clientId, limit = 100, windowSeconds = 60) {
  const key = `rate:${clientId}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, windowSeconds);
  return count > limit;
}

Problem: Boundary burst — client can make 100 requests at 1:59 and 100 more at 2:00, sending 200 in 2 seconds.

2. Sliding Window Counter (Recommended)

Approximates a true sliding window using weighted counts from current + previous window:

async function isRateLimitedSliding(clientId, limit = 100, windowSeconds = 60) {
  const now = Math.floor(Date.now() / 1000);
  const currWindow = Math.floor(now / windowSeconds);
  const prevWindow = currWindow - 1;
  const elapsed = now % windowSeconds;

  const [prevCount, currCount] = await redis.mget(
    `rate:${clientId}:${prevWindow}`,
    `rate:${clientId}:${currWindow}`
  );

  const weighted = (Number(prevCount) || 0) * (1 - elapsed / windowSeconds)
                 + (Number(currCount) || 0);
  if (weighted >= limit) return true;

  const pipeline = redis.pipeline();
  pipeline.incr(`rate:${clientId}:${currWindow}`);
  pipeline.expire(`rate:${clientId}:${currWindow}`, windowSeconds * 2);
  await pipeline.exec();
  return false;
}

3. Token Bucket

Bucket holds N tokens. Requests consume tokens. Bucket refills at a fixed rate. Allows short bursts while maintaining average rate.

// Token bucket in Redis using Lua (atomic)
const script = `
  local key, capacity, rate, now = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
  local data = redis.call('HMGET', key, 'tokens', 'ts')
  local tokens = tonumber(data[1]) or capacity
  local ts = tonumber(data[2]) or now
  tokens = math.min(capacity, tokens + (now - ts) * rate)
  if tokens < 1 then return 0 end
  redis.call('HMSET', key, 'tokens', tokens - 1, 'ts', now)
  redis.call('EXPIRE', key, 3600)
  return 1
`;

async function isAllowed(clientId) {
  return (await redis.eval(script, 1,
    `bucket:${clientId}`, 100, 1.67, Date.now()/1000)) === 1;
}

Rate Limit HTTP Headers

RateLimit-Limit: 100           # Max requests per window
RateLimit-Remaining: 42        # Remaining in this window
RateLimit-Reset: 1716854460    # Unix timestamp when window resets
Retry-After: 30               # Seconds to wait (on 429 response)

Express Middleware

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Global rate limit
app.use(rateLimit({
  windowMs: 60 * 1000,  max: 100,
  standardHeaders: true, legacyHeaders: false,
  store: new RedisStore({ client: redis }),
}));

// Stricter for sensitive endpoints
app.use('/auth/login', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  skipSuccessfulRequests: true,
}));

Client-Side: Handle 429 with Retry

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;
    const delay = (parseInt(res.headers.get('Retry-After')) || Math.pow(2, i)) * 1000;
    await new Promise(r => setTimeout(r, delay));
  }
  throw new Error('Max retries exceeded');
}

Rate Limiting Strategies

  • By IP: Simple but easy to bypass with proxies. For anonymous access.
  • By API key: More reliable, requires authentication.
  • By endpoint: Expensive operations get stricter limits (search: 10/min, reads: 1000/min).
  • Tiered: Different limits per subscription level (free/pro/enterprise).

→ Generate secure API key tokens with the Token Generator.