API Rate Limiting: Complete Guide for 2025
Why rate limiting matters, how token bucket and sliding window work, and how to return the right headers to clients.
Rate limiting is a critical component of API design. It protects your services from abuse, ensures fair resource usage, and prevents costly DDoS attacks. Whether you're building a public API or protecting internal services, understanding rate limiting is essential. Let's explore different rate limiting strategies, implementation patterns, and best practices for 2025.
What is Rate Limiting?
Rate limiting controls how many requests a client can make to your API within a specific time window. When a client exceeds the limit, the API returns a 429 Too Many Requests status code and typically includes headers indicating when the client can retry.
Rate limiting serves multiple purposes:
- Prevent abuse: Stop malicious users from overwhelming your API
- Ensure fairness: Distribute resources evenly among users
- Control costs: Limit expensive operations (database queries, external API calls)
- Maintain performance: Keep response times acceptable for all users
- Comply with quotas: Enforce usage tiers and billing limits
Rate Limiting Algorithms
1. Fixed Window
The simplest algorithm. Requests are counted within fixed time windows (e.g., 100 requests per minute). At the start of each window, the counter resets.
// Example: 100 requests per minute
// Window 1: 00:00 - 00:59 (100 requests allowed)
// Window 2: 01:00 - 01:59 (counter resets, 100 requests allowed)
// Problem: Burst at window boundaries
// User can make 100 requests at 00:59 and 100 more at 01:00Pros: Simple to implement, predictable behavior
Cons: Allows bursts at window boundaries, can be unfair
2. Sliding Window
Tracks requests in a rolling time window. More accurate than fixed window but requires more memory to track individual requests.
// Example: 100 requests per minute
// Track timestamps of last 100 requests
// If oldest request is < 1 minute ago, allow new request
// Otherwise, reject
// More accurate, prevents boundary burstsPros: More accurate, prevents boundary bursts
Cons: Higher memory usage, more complex implementation
3. Token Bucket
Maintains a bucket of tokens that refill at a constant rate. Each request consumes a token. If the bucket is empty, requests are rejected.
// Example: 10 tokens, refill 1 token per second
// Bucket capacity: 10 tokens
// Refill rate: 1 token/second
// User makes 5 requests → 5 tokens consumed, 5 remaining
// After 2 seconds → 2 tokens refilled, 7 total
// User can burst up to 10 requests if bucket is fullPros: Allows bursts, smooth rate limiting
Cons: More complex, requires precise timing
4. Leaky Bucket
Similar to token bucket but processes requests at a constant rate. Requests are queued and processed at a fixed rate, preventing bursts.
Pros: Smooth output rate, prevents bursts
Cons: Requires queuing, can delay requests
Identifying Clients for Rate Limiting
You need to identify who is making requests to apply rate limits. Common strategies:
1. By IP Address
Simplest approach—limit requests per IP address. Works for anonymous APIs but can be bypassed with proxies or VPNs.
const clientIp = req.ip || req.connection.remoteAddress;
const key = `rate_limit:${clientIp}`;2. By API Key
Most common for authenticated APIs. Each API key has its own rate limit, allowing different tiers (free, pro, enterprise).
const apiKey = req.headers['x-api-key'];
const key = `rate_limit:${apiKey}`;
// Different limits per tier
const limits = {
free: { requests: 100, window: 3600 }, // 100/hour
pro: { requests: 1000, window: 3600 }, // 1000/hour
enterprise: { requests: 10000, window: 3600 } // 10000/hour
};3. By User ID
For authenticated users, rate limit by user ID. Allows per-user quotas and prevents single users from consuming all resources.
4. By Endpoint
Different endpoints can have different rate limits. Expensive operations (search, AI processing) might have stricter limits than simple reads.
Implementing Rate Limiting
Using Redis (Recommended)
Redis is ideal for rate limiting because it's fast, supports atomic operations, and has built-in expiration. Here's a sliding window implementation:
const redis = require('redis');
const client = redis.createClient();
async function rateLimit(key, limit, window) {
const now = Date.now();
const windowStart = now - window * 1000;
// Remove old entries
await client.zremrangebyscore(key, 0, windowStart);
// Count current requests
const count = await client.zcard(key);
if (count >= limit) {
// Get oldest request timestamp
const oldest = await client.zrange(key, 0, 0, 'WITHSCORES');
const retryAfter = Math.ceil((parseInt(oldest[1]) + window * 1000 - now) / 1000);
return { allowed: false, retryAfter };
}
// Add current request
await client.zadd(key, now, now);
await client.expire(key, window);
return { allowed: true, remaining: limit - count - 1 };
}
// Usage
const result = await rateLimit(`rate_limit:${apiKey}`, 100, 3600);
if (!result.allowed) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: result.retryAfter
});
}Express.js Middleware
const express = require('express');
const rateLimit = require('express-rate-limit');
// Global rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
});
app.use('/api/', limiter);
// Per-route rate limiter
const strictLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 5, // 5 requests per minute
});
app.post('/api/login', strictLimiter, loginHandler);
// Custom key generator (by API key)
const apiKeyLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 1000,
keyGenerator: (req) => {
return req.headers['x-api-key'] || req.ip;
},
});Next.js API Routes
// pages/api/users.ts
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
});
export default async function handler(req, res) {
// Apply rate limiting
await new Promise((resolve, reject) => {
limiter(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
// Your API logic
res.status(200).json({ data: '...' });
}Rate Limit Headers
Always include rate limit information in response headers. This helps clients understand their limits and when they can retry:
// Standard headers (RFC 6585)
X-RateLimit-Limit: 100 // Total requests allowed
X-RateLimit-Remaining: 95 // Requests remaining
X-RateLimit-Reset: 1633024800 // Unix timestamp when limit resets
// When rate limited (429 response)
Retry-After: 60 // Seconds until retry allowed
// Example response
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1633024800
// Rate limited response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1633024800
Retry-After: 60
Content-Type: application/json
{
"error": "Too many requests",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"retryAfter": 60
}Best Practices
1. Use Appropriate Limits
Set limits based on your API's capacity and use case. Too strict limits frustrate legitimate users; too loose limits allow abuse.
- Read endpoints: Higher limits (1000-10000/hour)
- Write endpoints: Lower limits (100-1000/hour)
- Expensive operations: Very low limits (10-100/hour)
2. Implement Tiered Limits
Different user tiers should have different limits. Free users get basic limits, while paid users get higher limits.
3. Provide Clear Error Messages
When rate limited, return clear error messages explaining why and when the user can retry. Include Retry-After header.
4. Log Rate Limit Violations
Log when users hit rate limits. This helps identify abuse patterns and adjust limits if needed.
5. Use Distributed Rate Limiting
For multi-server deployments, use a shared store (Redis) for rate limiting. In-memory rate limiting only works per server.
6. Whitelist Internal Services
Internal services and admin endpoints might need higher limits or be exempt from rate limiting entirely.
Common Patterns
Burst Protection
Allow short bursts but limit sustained requests. Use token bucket algorithm for this.
Progressive Rate Limiting
Gradually reduce limits for users who repeatedly hit rate limits. This penalizes abusers while giving legitimate users second chances.
Endpoint-Specific Limits
Different endpoints have different limits. Expensive operations (AI processing, complex queries) have stricter limits.
Rate Limiting Services
If you don't want to implement rate limiting yourself, consider these services:
- Cloudflare - Rate limiting at the edge
- AWS API Gateway - Built-in rate limiting
- Kong - API gateway with rate limiting plugins
- NGINX - Rate limiting module
- Upstash - Serverless Redis with rate limiting
Conclusion
Rate limiting is essential for protecting your APIs and ensuring fair resource usage. Choose the right algorithm for your use case, implement it correctly with proper headers, and monitor violations to adjust limits as needed.
Remember: rate limiting is a balance between security and usability. Too strict limits frustrate users; too loose limits allow abuse. Start with reasonable defaults and adjust based on actual usage patterns.
Try these tools
Use these tools alongside this guide
Part of the ThenCatch blog. Learn more about us or browse more guides.