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.
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:
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:
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);
}
}Call 1 succeeded
Call 2 succeeded
Call 3 succeeded
Rate limit exceeded. Retry after 10s
Rate limit exceeded. Retry after 10sThe 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:
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:
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}`);
}
}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 500msThe bucket lets you burst up to 5 calls immediately, then enforces the steady rate of 2 per second.
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:
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:
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:
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:
// 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
| Algorithm | Best for | Weakness |
|---|---|---|
| Fixed window | Simple use cases, easy to explain | Burst at boundary |
| Sliding window | Smooth rate enforcement | Slightly more memory (stores timestamps) |
| Token bucket | Allowing bursts, steady long-term rate | More 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
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.
Frequently Asked Questions
What is the difference between rate limiting, debouncing, and throttling?
Should rate limiting happen on the client or server?
How do I handle rate-limited API responses?
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.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.