What Is Rate Limiting?
Rate limiting is one of those things that seems like an API provider's problem until you're building an API and a single buggy client is hammering your database at 500 requests per second at 3 AM. Then it becomes very much your problem. A 429 with a clear Retry-After header is the polite way to say "slow down" — and it protects everyone else using your service from paying for one client's mistake.
From the API provider's perspective, rate limiting is essential for fairness (preventing one client from monopolizing resources), stability (protecting against accidental or malicious traffic spikes), and cost management (controlling infrastructure costs). From the consumer's perspective, it's a constraint that needs to be designed around.
Common Rate Limiting Strategies
Fixed Window
The simplest approach: count requests within a fixed time window (e.g., 1,000 requests per hour). The counter resets at the start of each window. Simple to implement but has a known flaw: a burst of 1,000 requests at 11:59 PM followed by another 1,000 at 12:01 AM sends 2,000 requests in 2 minutes without hitting the limit.
Sliding Window
A more refined approach that tracks requests over the past N seconds/minutes, not within a fixed window boundary. This prevents the burst problem of fixed windows and provides smoother rate limiting, at the cost of more complex implementation (usually using Redis sorted sets).
Token Bucket
Requests consume tokens from a bucket. Tokens are added at a steady rate up to the bucket's capacity. If the bucket is empty, the request is rejected. This allows short bursts (using stored tokens) while maintaining a steady long-term rate — often the most user-friendly approach.
Leaky Bucket
Requests enter a queue and are processed at a fixed, steady rate. Unlike token bucket, there's no concept of burst capacity — traffic is always smoothed to the same rate. Good for downstream services that can't handle spikes.
Rate Limit Headers
Well-designed APIs include headers in every response to tell clients how many requests they have remaining and when limits reset. These headers vary by provider but common patterns include:
# GitHub-style headers X-RateLimit-Limit: 5000 # Total requests allowed per window X-RateLimit-Remaining: 4987 # Requests remaining in current window X-RateLimit-Reset: 1739232000 # Unix timestamp when window resets # When rate limited (429 response) Retry-After: 60 # Seconds until client can retry
Always read these headers proactively. Don't wait for a 429 — track your remaining quota and slow down before you hit the limit.
Implementing Retry Logic with Exponential Backoff
When you receive a 429, the worst thing to do is immediately retry in a loop. This hammers the server and delays your eventual success. Exponential backoff gradually increases the wait time between retries, giving the API room to recover:
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;
}
if (attempt === maxRetries) {
throw new Error('Max retries exceeded');
}
// Check Retry-After header first
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}The jitter (random milliseconds added to the delay) is important. Without it, multiple clients that all hit the rate limit at the same time will all retry at the same time, creating a synchronized retry storm that overwhelms the server again.
Proactive Rate Limit Management
Rather than hitting limits and backing off reactively, well-designed clients track their usage and throttle proactively:
class RateLimitedClient {
constructor(requestsPerMinute) {
this.requestsPerMinute = requestsPerMinute;
this.requestTimes = [];
}
async throttle() {
const now = Date.now();
const oneMinuteAgo = now - 60000;
// Remove timestamps older than 1 minute
this.requestTimes = this.requestTimes.filter(t => t > oneMinuteAgo);
if (this.requestTimes.length >= this.requestsPerMinute) {
// Wait until oldest request falls outside the window
const waitTime = this.requestTimes[0] + 60000 - now;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requestTimes.push(Date.now());
}
}Implementing Rate Limiting on Your API
If you're building an API, rate limiting is a must. The most practical approach for most applications is using middleware with Redis as the backend store:
// Express + express-rate-limit const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window standardHeaders: true, // Return RateLimit-* headers message: { error: 'rate_limit_exceeded', message: 'Too many requests. Please try again later.' } }); app.use('/api/', limiter);
Consider implementing tiered rate limits: free users get 100 requests/hour, paid users get 10,000, enterprise gets custom limits. Identify clients by their API key in the rate limiter. This makes rate limiting a feature of your pricing model rather than just a technical constraint.
Rate Limiting Best Practices
- Always include Retry-After: Tell clients exactly when they can try again
- Return headers proactively: Include remaining count and reset time on every response
- Be generous with limits: Overly aggressive limits hurt legitimate users more than bad actors (who use distributed IPs anyway)
- Limit by API key, not just IP: IP-based limiting affects users behind shared NATs
- Log and alert on repeated 429s: They often indicate bugs in client code
- Offer a way to request higher limits: Don't let rate limits silently block legitimate high-volume users
Conclusion
Rate limiting is a fundamental aspect of both API design and API consumption. As a consumer, always read rate limit headers, implement exponential backoff with jitter, and design your application to stay within limits proactively. As an API provider, use rate limiting to protect your infrastructure while being transparent with clients about limits and providing clear error messages and retry guidance. Done well, rate limiting makes APIs more reliable for everyone.