Rate Limiting in JavaScript: Complete Tutorial

Learn how to implement rate limiting in JavaScript to control how often a function or API call can execute. Protect your servers and respect third-party API limits.

7 min read

Rate limiting controls how many times an operation can run within a time window. If an API allows 100 requests per minute, a rate limiter enforces that limit by tracking calls and blocking or delaying requests that exceed it.

It protects servers from overload, respects third-party API quotas, and prevents a single client from consuming all resources.

The Simplest Rate Limiter: Fixed Window

Track calls within a fixed time window (like one minute). Reset the counter when the window expires:

javascriptjavascript
function createRateLimiter(maxCalls, windowMs) {
  let callCount = 0;
  let windowStart = Date.now();
 
  return function tryCall(fn) {
    const now = Date.now();
 
    // Reset if we are in a new window
    if (now - windowStart >= windowMs) {
      windowStart = now;
      callCount = 0;
    }
 
    if (callCount >= maxCalls) {
      const retryAfter = Math.ceil((windowStart + windowMs - now) / 1000);
      throw new Error(`Rate limit exceeded. Retry after ${retryAfter}s`);
    }
 
    callCount += 1;
    return fn();
  };
}

Usage:

javascriptjavascript
const limiter = createRateLimiter(3, 10000); // 3 calls per 10 seconds
 
for (let i = 1; i <= 5; i++) {
  try {
    const result = limiter(() => `Call ${i} succeeded`);
    console.log(result);
  } catch (err) {
    console.log(err.message);
  }
}
texttext
Call 1 succeeded
Call 2 succeeded
Call 3 succeeded
Rate limit exceeded. Retry after 10s
Rate limit exceeded. Retry after 10s

The first three calls go through. The fourth and fifth are blocked. After 10 seconds, the window resets.

The downside: all three allowed calls could happen in the first millisecond, then nothing for the rest of the window. This is the "burst at boundary" problem.

Token Bucket Algorithm

The token bucket fixes the burst problem by distributing tokens steadily over time. Each call consumes a token. Tokens refill at a constant rate:

javascriptjavascript
function createTokenBucket(maxTokens, refillRatePerSecond) {
  let tokens = maxTokens;
  let lastRefill = Date.now();
 
  return function tryCall(fn) {
    // Refill tokens based on elapsed time
    const now = Date.now();
    const elapsed = (now - lastRefill) / 1000;
    tokens = Math.min(maxTokens, tokens + elapsed * refillRatePerSecond);
    lastRefill = now;
 
    if (tokens < 1) {
      const waitTime = Math.ceil((1 - tokens) / refillRatePerSecond * 1000);
      throw new Error(`Rate limit exceeded. Retry after ${waitTime}ms`);
    }
 
    tokens -= 1;
    return fn();
  };
}

Usage:

javascriptjavascript
const bucket = createTokenBucket(5, 2); // Max 5 tokens, refill 2 per second
 
// Burst: use all 5 tokens at once
for (let i = 1; i <= 7; i++) {
  try {
    console.log(bucket(() => `Call ${i} ok`));
  } catch (err) {
    console.log(`Call ${i}: ${err.message}`);
  }
}
texttext
Call 1 ok
Call 2 ok
Call 3 ok
Call 4 ok
Call 5 ok
Call 6: Rate limit exceeded. Retry after 500ms
Call 7: Rate limit exceeded. Retry after 500ms

The bucket lets you burst up to 5 calls immediately, then enforces the steady rate of 2 per second.

Token bucket: tokens drain on calls, refill over time

Tokens drain one per call. They refill gradually as time passes. When the bucket is empty, calls are blocked until enough time has passed to accumulate at least one token.

Sliding Window Algorithm

The sliding window counts calls in the last N milliseconds, not in a fixed calendar window. This gives smoother rate limiting:

javascriptjavascript
function createSlidingWindow(maxCalls, windowMs) {
  const timestamps = [];
 
  return function tryCall(fn) {
    const now = Date.now();
 
    // Remove timestamps outside the window
    while (timestamps.length > 0 && timestamps[0] <= now - windowMs) {
      timestamps.shift();
    }
 
    if (timestamps.length >= maxCalls) {
      const retryAfter = Math.ceil((timestamps[0] + windowMs - now) / 1000);
      throw new Error(`Rate limit exceeded. Retry after ${retryAfter}s`);
    }
 
    timestamps.push(now);
    return fn();
  };
}

The sliding window is more precise. If you allow 10 calls per minute, you can never exceed 10 calls in any rolling 60-second period, not just per calendar minute.

API Rate Limiter with Retry Logic

Combine rate limiting with automatic retry when you hit a limit:

javascriptjavascript
async function rateLimitedFetch(url, options, limiter, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await limiter(() => fetch(url, options));
    } catch (err) {
      if (attempt === maxRetries) throw err;
 
      // Parse retry time from error message
      const match = err.message.match(/Retry after (\d+)(m?)s/);
      const waitMs = match
        ? parseInt(match[1]) * (match[2] ? 60000 : 1000)
        : Math.pow(2, attempt) * 1000; // Exponential backoff fallback
 
      console.log(`Rate limited. Retrying in ${waitMs}ms (attempt ${attempt + 1})`);
      await new Promise(resolve => setTimeout(resolve, waitMs));
    }
  }
}

Usage:

javascriptjavascript
const apiLimiter = createTokenBucket(10, 2); // 10 burst, 2/sec sustained
 
async function getUser(id) {
  return rateLimitedFetch(
    `https://api.example.com/users/${id}`,
    { headers: { Authorization: "Bearer token" } },
    apiLimiter
  );
}

This pairs with debouncing for user-triggered API calls and throttling for continuous events. Each pattern solves a different timing problem.

Server-Side Rate Limiting (Node.js)

Rate limiting is most important on the server. Here is a simple Express-style middleware:

javascriptjavascript
// Per-IP rate limiter store
const ipBuckets = new Map();
 
function rateLimitMiddleware(maxCalls, windowMs) {
  return (req, res, next) => {
    const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
 
    if (!ipBuckets.has(ip)) {
      ipBuckets.set(ip, createTokenBucket(maxCalls, maxCalls / (windowMs / 1000)));
    }
 
    const bucket = ipBuckets.get(ip);
 
    try {
      bucket(() => {}); // Consume a token
      next();
    } catch (err) {
      res.status(429).json({
        error: "Too Many Requests",
        retryAfter: err.message
      });
    }
  };
}

This creates one token bucket per IP address. Each request consumes a token. When a bucket is empty, the server responds with HTTP 429.

Choosing an Algorithm

AlgorithmBest forWeakness
Fixed windowSimple use cases, easy to explainBurst at boundary
Sliding windowSmooth rate enforcementSlightly more memory (stores timestamps)
Token bucketAllowing bursts, steady long-term rateMore complex to tune

Start with the fixed window. Move to token bucket when you need to allow bursts. Use sliding window when you need precise per-second enforcement.

Common Mistake: Client-Only Rate Limiting

Client-side rate limiting is a courtesy, not a security measure. Anyone can open DevTools and remove your rate limiter. Always enforce limits on the server. Use client-side limiting to provide better UX (like showing "too many requests" early instead of letting the server reject them).

Rune AI

Rune AI

Key Insights

  • Rate limiting restricts how many operations can happen in a time period.
  • The token bucket algorithm allows bursts while enforcing a long-term average rate.
  • The sliding window provides smoother rate enforcement than the fixed window.
  • Rate limiters work for both outgoing API calls and incoming request handlers.
  • Handle HTTP 429 responses with exponential backoff and retry logic.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between rate limiting, debouncing, and throttling?

Rate limiting controls how many times a function can run in a given time window (e.g., 100 requests per minute). Debouncing delays execution until activity stops (e.g., search input). Throttling enforces a minimum gap between executions (e.g., scroll handler). Rate limiting counts over a window; throttling spaces evenly.

Should rate limiting happen on the client or server?

Always enforce rate limits on the server. Client-side rate limiting is a UX improvement, not a security measure. Anyone can bypass client-side code.

How do I handle rate-limited API responses?

Check for HTTP 429 (Too Many Requests) responses. Read the Retry-After header. Implement exponential backoff: wait longer after each retry. Queue requests and process them with a delay.

Conclusion

Rate limiting protects both your servers and the APIs you call. Choose the right algorithm for your use case: token bucket for bursty traffic, sliding window for precise limits, or fixed window for simplicity. Always combine client-side limiting with server-side enforcement.A rate limiter counts operations over time and blocks when the count exceeds a threshold. The fixed window is the simplest. The token bucket allows bursts within a steady average. The sliding window provides the most precise enforcement. Use rate limiting for outgoing API calls to respect quotas. Use it on the server to protect against abuse. Combine it with retry logic and exponential backoff for resilient API clients.