Definition
429 Too Many Requests is an HTTP status code indicating that the client has sent too many requests in a given time period. This is part of rate limiting, a critical security and performance mechanism to prevent abuse, DoS attacks, and resource exhaustion.
The response should include:
- Retry-After Header - When the client can retry (seconds or HTTP date)
- Rate Limit Info - Current limits and reset time (e.g.,
X-RateLimit-*headers) - Error Details - Explanation of the rate limit policy
Common rate limit strategies:
- Fixed Window - 100 requests per hour (resets at the top of each hour)
- Sliding Window - 100 requests per rolling 60-minute window
- Token Bucket - Requests consume tokens; tokens refill at a constant rate
- Leaky Bucket - Requests queued and processed at a constant rate
429 is defined in RFC 6585 and is essential for protecting APIs from abuse and ensuring fair resource usage.
Example
429 Too Many Requests - Rate Limit Exceeded:
POST /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer YOUR_TOKEN
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1736424000
Content-Type: application/json
{
"error": "Too Many Requests",
"message": "Rate limit exceeded. You can make 100 requests per hour.",
"limit": 100,
"remaining": 0,
"resetAt": "2026-01-09T12:00:00Z",
"retryAfter": 3600
}
429 with HTTP Date in Retry-After:
GET /api/search?q=example HTTP/1.1
Host: api.example.com
HTTP/1.1 429 Too Many Requests
Retry-After: Thu, 09 Jan 2026 12:00:00 GMT
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1736424000
Content-Type: application/json
{
"error": "Too Many Requests",
"message": "Search rate limit exceeded. Try again after 2026-01-09T12:00:00Z.",
"limit": 10,
"remaining": 0,
"resetAt": "2026-01-09T12:00:00Z"
}
Code Example
JavaScript (Fetch API with Retry Logic):
const apiRequest = async (url, options = {}, maxRetries = 3) => {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, {
...options,
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json',
...options.headers
}
});
// Handle 429 Too Many Requests
if (response.status === 429) {
const error = await response.json();
console.warn('429 Too Many Requests:', error.message);
// Get retry timing from headers
const retryAfter = response.headers.get('Retry-After');
const ratelimitReset = response.headers.get('X-RateLimit-Reset');
let delayMs;
// Retry-After can be seconds or HTTP date
if (retryAfter) {
const retryAfterInt = parseInt(retryAfter);
if (isNaN(retryAfterInt)) {
// HTTP date format
const resetDate = new Date(retryAfter);
delayMs = resetDate.getTime() - Date.now();
} else {
// Seconds
delayMs = retryAfterInt * 1000;
}
} else if (ratelimitReset) {
// Unix timestamp
const resetDate = new Date(parseInt(ratelimitReset) * 1000);
delayMs = resetDate.getTime() - Date.now();
} else {
// Fallback: exponential backoff
delayMs = Math.min(1000 * Math.pow(2, attempt), 60000);
}
console.log(`Waiting ${delayMs}ms before retry (attempt ${attempt + 1}/${maxRetries})...`);
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delayMs));
continue; // Retry
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries - 1) {
console.error('Max retries reached:', error);
throw error;
}
console.error(`Attempt ${attempt + 1} failed:`, error.message);
}
}
};
// Example: Creating user with rate limit handling
const createUser = async (userData) => {
try {
const result = await apiRequest('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
console.log('User created:', result);
return result;
} catch (error) {
console.error('Failed to create user:', error);
throw error;
}
};
// Example: Checking rate limit headers before request
const checkRateLimit = async () => {
const response = await fetch('https://api.example.com/rate-limit', {
headers: {
'Authorization': 'Bearer YOUR_TOKEN'
}
});
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
console.log('Rate Limit:', {
limit,
remaining,
resetAt: new Date(parseInt(reset) * 1000).toISOString()
});
return {
limit: parseInt(limit),
remaining: parseInt(remaining),
resetAt: new Date(parseInt(reset) * 1000)
};
};
Python (requests library with Retry Logic):
import requests
import time
from datetime import datetime
def api_request(url, method='GET', json=None, max_retries=3):
headers = {
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json'
}
for attempt in range(max_retries):
try:
response = requests.request(
method,
url,
json=json,
headers=headers
)
# Handle 429 Too Many Requests
if response.status_code == 429:
error = response.json()
print(f'429 Too Many Requests: {error.get("message")}')
# Get retry timing from headers
retry_after = response.headers.get('Retry-After')
ratelimit_reset = response.headers.get('X-RateLimit-Reset')
delay_seconds = None
# Retry-After can be seconds or HTTP date
if retry_after:
try:
# Try parsing as integer (seconds)
delay_seconds = int(retry_after)
except ValueError:
# Parse as HTTP date
reset_date = datetime.strptime(retry_after, '%a, %d %b %Y %H:%M:%S %Z')
delay_seconds = (reset_date - datetime.now()).total_seconds()
elif ratelimit_reset:
# Unix timestamp
reset_date = datetime.fromtimestamp(int(ratelimit_reset))
delay_seconds = (reset_date - datetime.now()).total_seconds()
else:
# Fallback: exponential backoff
delay_seconds = min(2 ** attempt, 60)
print(f'Waiting {delay_seconds}s before retry (attempt {attempt + 1}/{max_retries})...')
# Wait before retrying
time.sleep(max(delay_seconds, 0))
continue # Retry
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
print(f'Max retries reached: {e}')
raise
print(f'Attempt {attempt + 1} failed: {e}')
# Example: Creating user with rate limit handling
def create_user(user_data):
try:
result = api_request(
'https://api.example.com/users',
method='POST',
json=user_data
)
print(f'User created: {result}')
return result
except Exception as error:
print(f'Failed to create user: {error}')
raise
# Example: Checking rate limit headers before request
def check_rate_limit():
response = requests.get(
'https://api.example.com/rate-limit',
headers={'Authorization': 'Bearer YOUR_TOKEN'}
)
limit = response.headers.get('X-RateLimit-Limit')
remaining = response.headers.get('X-RateLimit-Remaining')
reset = response.headers.get('X-RateLimit-Reset')
print('Rate Limit:', {
'limit': limit,
'remaining': remaining,
'resetAt': datetime.fromtimestamp(int(reset)).isoformat()
})
return {
'limit': int(limit),
'remaining': int(remaining),
'resetAt': datetime.fromtimestamp(int(reset))
}
Diagram
sequenceDiagram
participant Client
participant API
participant RateLimiter
Note over Client,RateLimiter: Initial Requests
Client->>API: Request 1
API->>RateLimiter: Check limit
RateLimiter-->>API: OK (99 remaining)
API->>Client: 200 OK
X-RateLimit-Remaining: 99
Client->>API: Request 2
API->>RateLimiter: Check limit
RateLimiter-->>API: OK (98 remaining)
API->>Client: 200 OK
X-RateLimit-Remaining: 98
Note over Client,RateLimiter: ... 97 more requests ...
Client->>API: Request 101
API->>RateLimiter: Check limit
RateLimiter-->>API: LIMIT EXCEEDED
API->>Client: 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Remaining: 0
Note over Client: Wait for Retry-After
Client->>Client: Sleep 3600s
Note over RateLimiter: Rate limit window resets
Client->>API: Request (after reset)
API->>RateLimiter: Check limit
RateLimiter-->>API: OK (99 remaining)
API->>Client: 200 OK
X-RateLimit-Remaining: 99
Security Notes
Analogy
Think of 429 like a restaurant with limited seating:
- Rate Limit β “We can only serve 100 customers per hour”
- 429 Response β “We’re at capacity. Come back in 30 minutes”
- Retry-After β “We’ll have a table available at 7:00 PM”
Without rate limiting, the restaurant (server) would be overwhelmed and couldn’t serve anyone properly.
Best Practices
- Always Include Retry-After - Tell clients when they can retry
- Use X-RateLimit- Headers* - Provide limit, remaining, and reset info
- Different Limits per Endpoint - More restrictive for expensive operations
- Track by User/IP/Key - Not globally (per-resource rate limiting)
- Implement Exponential Backoff - Clients should back off if retries fail
- Log Rate Limit Hits - Monitor for abuse patterns
- Document Rate Limits - Clearly document limits in API documentation
- Use 429, not 503 - Distinguish rate limiting from server errors
Common Mistakes
- No Retry-After Header - Not telling clients when to retry
- No Rate Limit Info - Not including X-RateLimit-* headers
- Global Rate Limiting - Not tracking per user/IP (allows unfair usage)
- Same Limit Everywhere - Not adjusting limits based on endpoint cost
- Using 503 Instead - Confusing rate limiting with server unavailability
- Not Logging Violations - Missing abuse patterns and attacks
- No Client Retry Logic - Clients not respecting Retry-After header
- Inconsistent Windows - Mixing fixed and sliding window strategies