DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Rate Limiting Strategies: Token Bucket, Sliding Window, and Fixed Window

Rate Limiting Strategies: Token Bucket, Sliding Window, and Fixed Window

Rate limiting protects your API from abuse, accidental DDoS, and runaway client bugs. Three algorithms dominate — here's when to use each.

Fixed Window

Count requests in fixed time windows (0:00-1:00, 1:00-2:00).

Window: 60 seconds, limit: 100 requests
00:00-01:00: 100 requests → blocked
01:00-02:00: counter resets → 100 more allowed

Problem: 100 requests at 00:59 + 100 at 01:00 = 200 requests in 2 seconds
Enter fullscreen mode Exit fullscreen mode
// Fixed window with Redis
async function fixedWindow(key: string, limit: number, windowMs: number) {
  const windowKey = `${key}:${Math.floor(Date.now() / windowMs)}`;
  const count = await redis.incr(windowKey);
  if (count === 1) await redis.pexpire(windowKey, windowMs);
  return count <= limit;
}
Enter fullscreen mode Exit fullscreen mode

Sliding Window

Track requests in a rolling window — no reset spike problem.

// Sliding window log
async function slidingWindow(key: string, limit: number, windowMs: number) {
  const now = Date.now();
  const windowStart = now - windowMs;

  // Remove old requests
  await redis.zremrangebyscore(key, '-inf', windowStart);

  // Count requests in window
  const count = await redis.zcard(key);
  if (count >= limit) return false;

  // Add current request
  await redis.zadd(key, now, `${now}-${Math.random()}`);
  await redis.pexpire(key, windowMs);
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Token Bucket

Tokens refill at a fixed rate. Requests consume tokens. Allows bursts up to bucket capacity.

// Token bucket — allows bursting
async function tokenBucket(
  key: string,
  capacity: number,   // Max tokens (burst limit)
  refillRate: number, // Tokens per second
) {
  const now = Date.now() / 1000; // seconds
  const bucket = await redis.hgetall(key);

  const tokens = parseFloat(bucket?.tokens ?? capacity.toString());
  const lastRefill = parseFloat(bucket?.lastRefill ?? now.toString());

  // Calculate new tokens
  const elapsed = now - lastRefill;
  const newTokens = Math.min(capacity, tokens + elapsed * refillRate);

  if (newTokens < 1) return false; // No tokens available

  // Consume a token
  await redis.hset(key, { tokens: newTokens - 1, lastRefill: now });
  await redis.expire(key, 3600);
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Express Middleware

function rateLimitMiddleware(limit: number, windowMs: number) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const key = `rate:${req.ip}`;
    const allowed = await slidingWindow(key, limit, windowMs);

    if (!allowed) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: Math.ceil(windowMs / 1000),
      });
    }
    next();
  };
}

// Apply globally or per-route
app.use('/api', rateLimitMiddleware(100, 60_000)); // 100 req/min
app.use('/api/auth', rateLimitMiddleware(10, 60_000)); // Stricter for auth
Enter fullscreen mode Exit fullscreen mode

Using @upstash/ratelimit (Easiest)

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, '1 m'),
  analytics: true,
});

const { success, limit, remaining, reset } = await ratelimit.limit(req.ip);
Enter fullscreen mode Exit fullscreen mode

Decision Guide

Use Case Algorithm
Simple API protection Fixed window
No burst spikes Sliding window
Allow burst traffic Token bucket
Edge/serverless @upstash/ratelimit

Rate limiting ships pre-configured (sliding window + Upstash) in the AI SaaS Starter Kit. $99 at whoffagents.com.

Top comments (0)