Originally published on API Status Check.
How to Handle API Rate Limits Gracefully (2026 Guide)
You're building an integration. Everything works beautifully in testing. Then production hits, traffic scales, and suddenly: HTTP 429 - Too Many Requests. Your app crashes. Your logs flood. Your users are blocked.
Sound familiar?
API rate limiting is one of the most common integration challenges developers face, yet many teams don't handle it until it becomes a crisis. This guide will show you how to handle rate limits gracefully from day one.
What Are Rate Limits and Why Do APIs Use Them?
Rate limiting is when an API restricts how many requests you can make within a time window. This protects the API provider's infrastructure from abuse and ensures fair resource distribution across all clients.
Common rate limit patterns:
- Fixed window: 100 requests per minute (resets at :00 seconds)
- Sliding window: 100 requests per rolling 60-second period
- Token bucket: Requests consume tokens; tokens refill over time
- Concurrent requests: Maximum 10 simultaneous connections
Why providers enforce limits:
- Infrastructure protection: Prevents single clients from overwhelming servers
- Fair usage: Ensures all customers get reliable service
- Cost management: API calls cost money (compute, database queries, third-party services)
- Business model: Higher tiers pay for higher limits
When you hit a rate limit, the API typically responds with:
-
Status code:
429 Too Many Requests - Headers: Information about your limit and when it resets
- Body: Error message explaining the limit
Understanding Rate Limit Headers
Before implementing strategies, you need to read what the API is telling you. Most modern APIs follow these header conventions:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1643723400
Retry-After: 60
Key headers:
-
X-RateLimit-Limit: Total requests allowed in the window -
X-RateLimit-Remaining: Requests left before hitting the limit -
X-RateLimit-Reset: Unix timestamp when the limit resets -
Retry-After: Seconds to wait before retrying (some APIs use this instead)
Pro tip: Check these headers on every response, not just 429s. This lets you proactively slow down before hitting the limit.
Strategy 1: Exponential Backoff with Jitter
Exponential backoff means doubling your wait time after each failure. Jitter adds randomness to prevent thundering herd problems (many clients retrying simultaneously).
This is the gold standard for retry logic.
async function fetchWithExponentialBackoff(url, options = {}, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, options);
// Success - return response
if (response.ok) {
return response;
}
// Rate limited - calculate backoff
if (response.status === 429) {
attempt++;
if (attempt >= maxRetries) {
throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
}
// Check for Retry-After header
const retryAfter = response.headers.get('Retry-After');
let waitTime;
if (retryAfter) {
// Retry-After can be seconds or HTTP date
waitTime = parseInt(retryAfter) * 1000 ||
new Date(retryAfter).getTime() - Date.now();
} else {
// Exponential backoff: 2^attempt * 1000ms, with jitter
const exponentialDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000; // 0-1000ms random
waitTime = exponentialDelay + jitter;
}
console.log(`Rate limited. Retrying in ${waitTime}ms (attempt ${attempt}/${maxRetries})`);
await sleep(waitTime);
continue;
}
// Other error - throw
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
if (attempt >= maxRetries - 1) throw error;
attempt++;
const waitTime = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
await sleep(waitTime);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Why this works:
- Respects
Retry-Afterwhen provided - Backs off exponentially: 2s → 4s → 8s → 16s
- Jitter prevents synchronized retries across clients
- Configurable max retries prevents infinite loops
Strategy 2: Request Queuing with Token Bucket
Instead of firing requests immediately and handling failures, queue requests and control the rate proactively. This is ideal for batch processing or high-volume scenarios.
class RateLimiter {
constructor(tokensPerInterval, interval) {
this.tokensPerInterval = tokensPerInterval; // e.g., 100
this.interval = interval; // e.g., 60000 (1 minute)
this.tokens = tokensPerInterval;
this.queue = [];
// Refill tokens periodically
setInterval(() => {
this.tokens = this.tokensPerInterval;
this.processQueue();
}, this.interval);
}
async execute(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.processQueue();
});
}
processQueue() {
while (this.queue.length > 0 && this.tokens > 0) {
const { fn, resolve, reject } = this.queue.shift();
this.tokens--;
fn()
.then(resolve)
.catch(reject);
}
}
}
// Usage
const limiter = new RateLimiter(100, 60000); // 100 requests per minute
async function fetchUsers(userIds) {
const results = await Promise.all(
userIds.map(id =>
limiter.execute(() =>
fetch(`https://api.example.com/users/${id}`).then(r => r.json())
)
)
);
return results;
}
Benefits:
- Prevents 429 errors before they happen
- Smooth, predictable request flow
- Great for background jobs and batch operations
- Can be extended with priority queues
Trade-off: Adds complexity and potential latency. Best for non-interactive workloads.
Strategy 3: Response Caching
The fastest way to avoid rate limits? Don't make the request at all.
Caching is often overlooked but incredibly effective, especially for:
- Configuration data that changes rarely
- User profiles
- Public data (weather, stock prices)
- Search results
class CachedAPIClient {
constructor(ttlMs = 300000) { // 5 minutes default
this.cache = new Map();
this.ttl = ttlMs;
}
async get(url) {
const cached = this.cache.get(url);
// Return cached if valid
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log('Cache hit:', url);
return cached.data;
}
// Fetch fresh data
console.log('Cache miss:', url);
const response = await fetch(url);
const data = await response.json();
// Store with timestamp
this.cache.set(url, {
data,
timestamp: Date.now()
});
return data;
}
invalidate(url) {
this.cache.delete(url);
}
clear() {
this.cache.clear();
}
}
// Usage
const api = new CachedAPIClient(60000); // 1 minute TTL
// First call hits API
const user1 = await api.get('https://api.example.com/user/123');
// Second call (within 1 min) uses cache - no API call!
const user2 = await api.get('https://api.example.com/user/123');
Advanced caching strategies:
- Redis/Memcached: Share cache across servers
- ETags: Server tells you if data changed (304 Not Modified)
- Cache-Control headers: Respect server-side caching hints
- Stale-while-revalidate: Serve stale data while fetching fresh in background
Rate Limits of Popular APIs
Here's a quick reference for common APIs (as of 2026):
| API | Free Tier | Paid Tier | Reset Window | Notes |
|---|---|---|---|---|
| OpenAI | 3 RPM (GPT-4) | 500+ RPM | 1 minute | Token-based limits also apply |
| Stripe | 100 RPS | 100 RPS (all tiers) | 1 second | Rate limits by request type |
| GitHub | 60 RPH | 5,000 RPH | 1 hour | GraphQL has separate limits |
| Discord | Varies by endpoint | Same | Varies | Global: 50/sec, DM: 1/sec per channel |
| Twilio | 1 RPS | 30-100 RPS | 1 second | Varies by message type |
| Google Maps | 40,000 per month | Pay-as-you-go | Monthly | ~50 requests per second |
| Twitter/X | 500,000 per month | Varies | Monthly | v2 API, Basic tier |
Legend: RPM = Requests Per Minute, RPS = Requests Per Second, RPH = Requests Per Hour
Always check the official documentation - limits change frequently!
Conclusion: Build Resilient Integrations
Rate limits aren't bugs - they're features that protect infrastructure and ensure fair access. The best developers:
- Read the documentation - Know your limits before you hit them
- Implement backoff strategies - Retry intelligently with exponential backoff
- Cache aggressively - Don't make requests you don't need
- Monitor proactively - Track usage and set alerts
- Plan for scale - Design systems that degrade gracefully
Start implementing these strategies today, and you'll never lose sleep over 429 errors again.
Want automatic rate limit monitoring? API Status Check tracks rate limits across all your API integrations and alerts you before you hit the wall. Set up your first check in under 2 minutes.
Top comments (0)