HTTP 429 Too Many Requests: Rate Limiting Explained
The 429 Too Many Requests status is the server's signal: "you are too fast, slow down." It is the standard response from a rate limiter protecting API документацию and websites from abuse. Let us cover how to implement 429 correctly on the server, how a client should handle Retry-After, and what to do when your service receives 429 from an external API.
What 429 Means
Per RFC 6585 §4, 429 indicates the user has sent too many requests in a given time. Unlike 503 (service unavailable) or 403 (forbidden by policy), 429 is a temporary state — the client may retry after Retry-After.
Mandatory header: Retry-After: 60 (seconds) or a date. Recommended informal headers:
X-RateLimit-Limit— max requests per windowX-RateLimit-Remaining— remaining requestsX-RateLimit-Reset— Unix timestamp of window reset
Rate Limiting Strategies
Token Bucket
A bucket of tokens refills at a fixed rate. Each request consumes a token. Empty = 429.
Leaky Bucket
A fixed-size queue drained at a constant rate. Overflow = 429.
Fixed Window
Counter per minute/hour. Simple, but allows bursts at window boundaries.
Sliding Window
Hybrid: accounts for the previous window proportionally. The smoothest approach.nginx Configuration
# Zone: 10MB fits ~160k unique IPs
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}Parameters:
rate=60r/m— 1 request per second on averageburst=20— allow bursts up to 20, drop the restnodelay— do not delay, return 429 immediately on overflow
Application-Level Rate Limiting (Redis sliding window)
<?php
function checkRateLimit(Redis $redis, string $key, int $max, int $window): array {
$now = microtime(true);
$pipe = $redis->multi();
$pipe->zRemRangeByScore($key, 0, $now - $window);
$pipe->zAdd($key, $now, "$now:" . bin2hex(random_bytes(4)));
$pipe->zCard($key);
$pipe->expire($key, $window * 2);
$results = $pipe->exec();
$count = $results[2];
return [
'allowed' => $count <= $max,
'limit' => $max,
'remaining' => max(0, $max - $count),
'reset' => time() + $window,
];
}
// Usage
$rl = checkRateLimit($redis, "rl:user:{$userId}", 60, 60);
header("X-RateLimit-Limit: {$rl['limit']}");
header("X-RateLimit-Remaining: {$rl['remaining']}");
header("X-RateLimit-Reset: {$rl['reset']}");
if (!$rl['allowed']) {
http_response_code(429);
header('Retry-After: 60');
header('Content-Type: application/json');
echo json_encode(['error' => 'Too many requests']);
exit;
}Client-side 429 Handling (exponential backoff)
async function fetchWithRetry(url, options = {}, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const retryAfter = res.headers.get('Retry-After');
const waitMs = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(1000 * Math.pow(2, attempt), 60000);
console.warn(`429 received, retrying in ${waitMs}ms`);
await new Promise(r => setTimeout(r, waitMs));
}
throw new Error('Rate limit exceeded after retries');
}Never retry aggressively without backoff — it worsens the rate limit and may trigger a WAF ban.
Client Identification: IP, User, API Key
The right tracker depends on context:
- APIs with keys — limit per API key
- Login endpoints — per IP (prevents brute force)
- User actions — per user_id (so NAT does not block many users sharing one IP)
// Hierarchy: user ID > API key > IP
function getRateLimitKey(Request $req): string {
if ($req->user?->id) return "user:{$req->user->id}";
if ($req->apiKey) return "apikey:{$req->apiKey}";
return "ip:{$req->ip}";
}Getting 429 from External API: What to Do
- Read
Retry-After— it is not a suggestion, it is a requirement. - Use a circuit breaker — temporarily stop hitting the degrading API.
- Cache responses in Redis with a sensible TTL.
- Batch requests when the API supports it.
- Request a limit increase — paid tiers usually offer more.
To monitor your API availability, use Enterno.io Monitors with expected_code=200 — detect 429 regressions instantly.
429 vs 503 vs 403
| Code | When to Use |
|---|---|
| 429 | Client exceeded rate limit |
| 503 | Server overloaded (general, not client-specific) |
| 403 | Access policy (client fully blocked) |
Frequently Asked Questions
Q: Does 429 affect SEO?
A: For Googlebot — yes, if frequent. Googlebot reduces crawl rate. Honor Retry-After and never block official Googlebot IPs.
Q: What is an adequate API rate limit?
A: It depends. Typical values: 60 req/min for general APIs, 5 req/min for login, 10 req/sec for realtime endpoints.
Q: Can 429 be bypassed?
A: Legit path: request a higher limit, optimize requests, cache. Bypassing via IP rotation = ToS violation and ban risk.
Conclusion
429 protects your service and partners. Configure a proper rate limit (sliding window + Redis), always return Retry-After and X-RateLimit-* headers, and use exponential backoff on the client. To monitor your API endpoints, use Enterno.io.
Check your website right now
Check now →