REST API Rate Limiting: Token Bucket, Sliding Window & Implementation
Protect your REST API from abuse with rate limiting. Learn token bucket, sliding window, and fixed window algorithms with real implementation examples and standard headers.
What you'll learn
- ✓Why rate limiting matters and what problems it solves
- ✓How token bucket, sliding window, and fixed window algorithms work
- ✓Standard rate limit headers (RateLimit draft RFC)
- ✓How to implement rate limiting in Node.js with Redis
- ✓Client-side strategies for handling 429 responses
Prerequisites
- •Basic understanding of REST APIs and HTTP
- •Familiarity with Node.js or similar backend language
Rate limiting controls how many requests a client can make to your API within a given time window. Without it, a single misbehaving client, a bot swarm, or even a bug in a mobile app can bring your entire service down.
This article covers the three most common algorithms, the headers your API should return, and a working implementation you can drop into a Node.js service.
Why rate limit?
Rate limiting solves several problems at once:
- Availability — prevents a single client from monopolizing server resources
- Fairness — ensures all clients get a reasonable share of capacity
- Cost control — protects downstream services and databases from overload
- Security — slows down brute-force attacks and credential stuffing
- Compliance — many third-party APIs require you to enforce limits on your own consumers
The three main algorithms
1. Fixed window
The simplest approach. Divide time into fixed intervals (e.g., 1-minute windows) and count requests per client in each window.
Window: 14:00:00 – 14:00:59 → limit: 100 requests
Window: 14:01:00 – 14:01:59 → counter resets
Pros: Simple to implement, low memory usage.
Cons: Vulnerable to burst at window boundaries. A client can send 100 requests at 14:00:59 and another 100 at 14:01:00 — 200 requests in 2 seconds.
// Fixed window in-memory (illustration only)
const windows = new Map();
function fixedWindowCheck(clientId, limit, windowSizeMs) {
const now = Date.now();
const windowKey = Math.floor(now / windowSizeMs);
const key = `${clientId}:${windowKey}`;
const current = windows.get(key) || 0;
if (current >= limit) {
return { allowed: false, remaining: 0 };
}
windows.set(key, current + 1);
return { allowed: true, remaining: limit - current - 1 };
}
2. Sliding window log
Track the timestamp of every request. When a new request arrives, count how many timestamps fall within the last N seconds.
const logs = new Map();
function slidingWindowLogCheck(clientId, limit, windowMs) {
const now = Date.now();
const windowStart = now - windowMs;
if (!logs.has(clientId)) logs.set(clientId, []);
const timestamps = logs.get(clientId);
// Remove expired entries
while (timestamps.length > 0 && timestamps[0] <= windowStart) {
timestamps.shift();
}
if (timestamps.length >= limit) {
return { allowed: false, remaining: 0 };
}
timestamps.push(now);
return { allowed: true, remaining: limit - timestamps.length };
}
Pros: No boundary burst problem — the window truly slides.
Cons: High memory usage. Storing every timestamp for every client gets expensive at scale.
3. Token bucket
The most popular algorithm in production. Each client has a “bucket” that holds tokens. Tokens are added at a fixed rate. Each request consumes one token. If the bucket is empty, the request is rejected.
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity; // max tokens
this.tokens = capacity; // start full
this.refillRate = refillRate; // tokens per second
this.lastRefill = Date.now();
}
tryConsume() {
this.refill();
if (this.tokens < 1) {
return { allowed: false, remaining: 0 };
}
this.tokens -= 1;
return { allowed: true, remaining: Math.floor(this.tokens) };
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
}
}
Pros: Allows short bursts (up to bucket capacity) while enforcing a sustained rate. Simple math, low memory.
Cons: Slightly more complex than fixed window. Requires storing two values per client (tokens and last refill time).
Choosing an algorithm
| Factor | Fixed Window | Sliding Window | Token Bucket |
|---|---|---|---|
| Burst handling | Poor | Good | Good (controlled) |
| Memory usage | Low | High | Low |
| Implementation | Simple | Moderate | Moderate |
| Fairness | Moderate | High | High |
| Production use | Small APIs | Analytics | Most APIs |
Rule of thumb: Use token bucket for general-purpose rate limiting. Use sliding window when you need precise per-second accuracy (e.g., billing).
Standard rate limit headers
The IETF draft RFC (draft-ietf-httpapi-ratelimit-headers) defines three headers your API should return on every response:
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 58
| Header | Meaning |
|---|---|
RateLimit-Limit | Maximum requests allowed in the current window |
RateLimit-Remaining | How many requests the client has left |
RateLimit-Reset | Seconds until the limit resets |
When a client exceeds the limit, return 429 Too Many Requests with a Retry-After header:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
{
"type": "https://api.example.com/errors/rate-limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "You have exceeded 100 requests per minute. Try again in 30 seconds."
}
Production implementation with Redis
In-memory rate limiting breaks in multi-server deployments. Use Redis for shared state.
import express from 'express';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const app = express();
async function tokenBucketMiddleware(req, res, next) {
const clientId = req.headers['x-api-key'] || req.ip;
const key = `ratelimit:${clientId}`;
const capacity = 100;
const refillRate = 100 / 60; // 100 per minute
// Lua script for atomic token bucket
const luaScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])
if tokens == nil then
tokens = capacity
last_refill = now
end
local elapsed = (now - last_refill) / 1000
tokens = math.min(capacity, tokens + elapsed * refill_rate)
if tokens < 1 then
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 120)
return {0, math.ceil((1 - tokens) / refill_rate)}
end
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 120)
return {1, math.floor(tokens)}
`;
const [allowed, value] = await redis.eval(
luaScript, 1, key, capacity, refillRate, Date.now()
);
res.set('RateLimit-Limit', String(capacity));
if (!allowed) {
const retryAfter = value;
res.set('RateLimit-Remaining', '0');
res.set('RateLimit-Reset', String(retryAfter));
res.set('Retry-After', String(retryAfter));
return res.status(429).json({
type: 'https://api.example.com/errors/rate-limited',
title: 'Rate limit exceeded',
status: 429,
detail: `Try again in ${retryAfter} seconds.`,
});
}
res.set('RateLimit-Remaining', String(value));
res.set('RateLimit-Reset', '60');
next();
}
app.use(tokenBucketMiddleware);
app.get('/api/resources', (req, res) => {
res.json({ data: ['item1', 'item2'] });
});
app.listen(3000);
The Lua script runs atomically inside Redis, so there are no race conditions between multiple app servers.
Tiered rate limits
Most production APIs apply different limits to different plans:
const RATE_LIMITS = {
free: { capacity: 60, window: 60 }, // 60/min
starter: { capacity: 600, window: 60 }, // 600/min
business: { capacity: 6000, window: 60 }, // 6000/min
enterprise: { capacity: 60000, window: 60 }, // 60000/min
};
function getLimits(req) {
const plan = req.user?.plan || 'free';
return RATE_LIMITS[plan];
}
You can also apply separate limits per endpoint — write endpoints get a lower limit than read endpoints:
const ENDPOINT_LIMITS = {
'GET /api/users': { capacity: 100, window: 60 },
'POST /api/users': { capacity: 10, window: 60 },
'DELETE /api/users': { capacity: 5, window: 60 },
};
Client-side: handling 429 responses
Good API clients respect rate limits. Implement exponential backoff with jitter:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
// Use Retry-After header if available
const retryAfter = response.headers.get('Retry-After');
const waitMs = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000);
console.warn(`Rate limited. Retrying in ${waitMs}ms (attempt ${attempt + 1})`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
throw new Error('Max retries exceeded due to rate limiting');
}
Common mistakes
- Rate limiting by IP only — shared IPs (offices, VPNs) punish innocent users. Use API keys when possible.
- No headers — clients cannot self-regulate without
RateLimit-Remaining. - Generic 429 body — always tell clients when they can retry.
- In-memory counters in a cluster — each server tracks separately, so the effective limit is
limit * server_count. - No rate limiting on auth endpoints — login and password reset endpoints are prime targets for brute force.
Summary
Rate limiting is not optional for any API exposed to the internet. Start with a token bucket backed by Redis, return the standard RateLimit-* headers on every response, and give clients a clear Retry-After value when they hit the limit. Layer tiered limits by plan and per-endpoint limits by operation cost, and your API will stay healthy under pressure.
Related articles
- REST APIs REST API Pagination Best Practices: Offset, Cursor & Keyset
Choose the right pagination strategy for your REST API. Compare offset, cursor, and keyset pagination with real examples, HATEOAS links, and performance trade-offs.
- REST APIs REST API Security Checklist: OWASP, Auth, CORS & Input Validation
A practical security checklist for REST APIs covering OWASP API Top 10, authentication, authorization, input validation, CORS, and common vulnerabilities with fixes.
- REST APIs REST API Webhooks Design: Patterns, Retries & Security
Design reliable webhooks for your REST API. Learn delivery patterns, retry logic with exponential backoff, HMAC signature verification, and idempotent event handling.
- REST APIs REST API Authentication: API Keys, JWT, and OAuth 2.0
Learn the three most common REST API authentication methods. Compare API keys, JWT tokens, and OAuth 2.0 with working code examples and security best practices.