API Rate Limiting Strategies Explained (With Node.js Examples)

Rate limiting protects your API from abuse, accidental infinite loops in client code, and traffic spikes that could take down your database. There are three algorithms worth knowing: fixed window, sliding window, and token bucket. This guide explains each one and shows a working Express.js middleware implementation.

Fixed Window

Fixed window counts requests within a discrete time bucket, like "0-60 seconds" then "60-120 seconds". It resets the counter to zero at each boundary. It is the simplest algorithm to implement but has a burst problem: a client can send the full limit right before a window ends, then the full limit again right after — doubling the effective rate for a short period.

const requestCounts = new Map(); // key -> { count, windowStart }
const WINDOW_MS = 60_000;
const LIMIT = 100;

function fixedWindowLimiter(req, res, next) {
  const key = req.ip;
  const now = Date.now();
  const entry = requestCounts.get(key);

  if (!entry || now - entry.windowStart >= WINDOW_MS) {
    requestCounts.set(key, { count: 1, windowStart: now });
    return next();
  }

  if (entry.count >= LIMIT) {
    res.set('Retry-After', Math.ceil((entry.windowStart + WINDOW_MS - now) / 1000));
    return res.status(429).json({ error: 'Too many requests' });
  }

  entry.count++;
  next();
}

Sliding Window

Sliding window fixes the boundary-burst problem by tracking a rolling time period instead of a fixed bucket. A simple approach stores timestamps and counts how many fall within the last N milliseconds.

const timestamps = new Map(); // key -> array of request times
const WINDOW_MS = 60_000;
const LIMIT = 100;

function slidingWindowLimiter(req, res, next) {
  const key = req.ip;
  const now = Date.now();
  const arr = (timestamps.get(key) || []).filter(t => now - t < WINDOW_MS);

  if (arr.length >= LIMIT) {
    res.set('Retry-After', '1');
    return res.status(429).json({ error: 'Too many requests' });
  }

  arr.push(now);
  timestamps.set(key, arr);
  next();
}

This is more accurate but uses more memory per key since it stores individual timestamps. In production, this state is usually kept in Redis with sorted sets (ZADD / ZREMRANGEBYSCORE) instead of an in-process Map, so the limit applies correctly across multiple server instances.

Token Bucket

Token bucket models a bucket that holds up to N tokens. Tokens refill at a steady rate, and each request consumes one token. If the bucket is empty, the request is rejected. This allows short bursts (spending saved-up tokens) while still enforcing a long-term average rate — closer to how real clients actually behave.

const buckets = new Map(); // key -> { tokens, lastRefill }
const CAPACITY = 20;
const REFILL_RATE = 5; // tokens per second

function tokenBucketLimiter(req, res, next) {
  const key = req.ip;
  const now = Date.now();
  let bucket = buckets.get(key) || { tokens: CAPACITY, lastRefill: now };

  const elapsedSeconds = (now - bucket.lastRefill) / 1000;
  bucket.tokens = Math.min(CAPACITY, bucket.tokens + elapsedSeconds * REFILL_RATE);
  bucket.lastRefill = now;

  if (bucket.tokens < 1) {
    buckets.set(key, bucket);
    res.set('Retry-After', '1');
    return res.status(429).json({ error: 'Too many requests' });
  }

  bucket.tokens -= 1;
  buckets.set(key, bucket);
  next();
}

Choosing an Algorithm

  • Fixed window — simplest, good enough for internal tools or low-stakes endpoints
  • Sliding window — accurate and fair, best for public APIs with strict SLAs
  • Token bucket — best when you want to allow legitimate bursts (e.g. a user syncing many items at once) without raising the sustained limit
  • In production, prefer battle-tested libraries like express-rate-limit or rate-limiter-flexible backed by Redis instead of hand-rolled in-memory maps, which break under multiple server instances

Frequently Asked Questions

What is the difference between fixed window and sliding window rate limiting?

Fixed window counts requests in discrete time buckets, like per minute, which can allow a burst of 2x the limit right at the window boundary. Sliding window smooths this out by counting requests over a rolling time period, giving a more accurate and fair limit.

What HTTP status code should a rate-limited API return?

Return 429 Too Many Requests, along with a Retry-After header telling the client how many seconds to wait before retrying.

Why is token bucket a popular choice for API rate limiting?

Token bucket allows short bursts of traffic up to the bucket capacity while still enforcing a steady average rate over time, which matches real-world client behavior better than a strict fixed cap.

Debug Errors Faster with AI

Seeing unexpected 429 responses or rate-limiter bugs in production? Paste the error into our free AI Error Explainer to get a clear cause and fix.

Related articles