HTTP 429 Too Many Requests: Identifying the Source and Resolving It
A 429 error can originate from your server, the CDN in front, or an external API. Learn to distinguish between them using response headers and implement a client that waits intelligently instead of hammering the endpoint.
UptimeMag editorial team · 14 October 2026 · 6 min read

When a site responds with 429, the first question isn't "why", it's "who": your Nginx instance, the CDN or WAF in front, or an external API the site queries (payments, maps, email). You can determine this in thirty seconds by examining the response headers before touching any configuration.
What the error means
A 429 isn't a broken server. According to RFC 6585, the code indicates that a client has sent too many requests within a given time interval: it's a traffic rule, not a failure. Anyone receiving a 429 should stop sending requests, wait, and retry — not insist.
Quickest check: look at the headers
With curl -I https://example.com/path (or curl -v to see the request too), you can read the 429 response headers. Three clues identify the source:
- Server: cloudflare or a
cf-rayheader → the block comes from the CDN/WAF in front, not your webserver. - Server: nginx and no third-party headers → the limit is in your Nginx configuration.
- Headers like
x-ratelimit-remaining,x-ratelimit-resetorRateLimit-*→ the response comes from an external API your code is calling, not the site itself.
The most important header, when present, is Retry-After. According to RFC 9110 §10.2.3, Retry-After tells the client how long to wait before retrying, and it's defined in RFC 9110 (IETF, June 2022) and referenced in RFC 6585 (IETF, April 2012) for 429 responses. The value can be expressed in two ways, both valid per the standard: as an integer number of seconds to wait from the response moment, or as an absolute HTTP date.
Cause 1: rate limit set on your own server (most common)
How to tell if it's yours. On Ubuntu 24.04 with Nginx: sudo tail -f /var/log/nginx/error.log while the site receives traffic. The key line starts with [error] and contains "limiting requests, excess": it means the limit_req module rejected a request because it exceeded the configured threshold, showing the zone name and client IP involved.
One detail that changes diagnosis: by default Nginx doesn't respond 429 to those exceeding the limit, it responds 503 Service Unavailable. You only get 429 by explicitly setting limit_req_status 429; in the server or location block. If your monitoring shows 503 instead of 429 but "limiting requests" appears in the log, it's still this issue: the difference is only in the returned code, not the cause.
On cPanel/Plesk with Apache, the equivalent mechanism is mod_ratelimit or ModSecurity/Fail2ban rules the control panel applies to IPs exceeding a request threshold: in that case check the ModSecurity log or active Fail2ban rules, not Nginx's error log.
How to resolve it. If the limit is too tight for real traffic (legitimate bots, a SPA doing polling, a cron querying too often), raise the threshold in the limit_req_zone directive or add a burst margin:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
}
burst=20 accepts up to 20 excess requests before refusing, nodelay serves them immediately instead of queuing.
How to verify it's fixed. Repeat the traffic that previously failed (the same script, the same bot) and check that the error log no longer produces "limiting requests" lines for that client, and that curl -I on the same resource returns 200 instead of 429/503.
Cause 2: rate limit applied by the CDN or WAF in front
How to tell if it's yours. If the 429 response comes with Server: cloudflare header or a cf-ray in the headers, your Nginx never even saw the request: the block happened upstream at the perimeter. In this case tail on your local error log won't show anything, because the request never reached your origin server.
How to resolve it. Log into the CDN/WAF dashboard and check the active rate limiting rule on that path: threshold, time window, action (block/challenge). If the blocked traffic is legitimate (a partner, monitoring service, webhook), add an exception for that IP or user-agent instead of raising the global limit, which would weaken protection for everyone.
How to verify it's fixed. Repeat the request from the same IP/client that previously got 429 and check that the response no longer contains the typical CDN block header (this varies by provider: check the rule status page).
Cause 3: an external API the site calls is rate-limiting you
How to tell if it's yours. Here, the 429 isn't received by the site visitor, it's received by your code when calling a third-party service (payment gateway, maps API, email service). Verify this in application logs (PHP-FPM slow log, application log) by searching for outgoing HTTP calls failing with 429, and reading the response headers from that API.
A concrete example of how these limits work, from GitHub's official documentation: the primary limit for unauthenticated requests is 60 requests per hour, while using a personal access token raises the limit to 5,000 requests per hour. This is a "sliding window" limit: it counts requests in a fixed interval and resets when it expires. Real-time status is read from every response's headers: x-ratelimit-limit shows the hourly maximum, x-ratelimit-remaining shows remaining requests in the current window, x-ratelimit-reset shows when the window resets, in seconds since epoch UTC.
GitHub also adds a second control layer, easy to overlook: no more than 100 concurrent requests are allowed, and no more than 900 points per minute across REST API endpoints. Staying under the declared hourly limit might still result in 429 for this second limit if you send too many parallel or tightly spaced requests: this is the most common cause of "unexplained" 429s on integrations that seem to respect quota.
How to resolve it. Don't increase anything on your end: slow down. Reduce call concurrency, introduce caching for responses that don't change often, and above all respect the header when the API sends it.
A client respecting Retry-After and using exponential backoff when the header is absent:
import time
import random
def call_with_backoff(make_request, max_attempts=6):
wait = 1
for attempt in range(max_attempts):
response = make_request()
if response.status_code != 429:
return response
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
seconds = int(retry_after) if retry_after.isdigit() else 5
else:
seconds = wait
time.sleep(seconds + random.uniform(0, 1))
wait = wait * 2
raise RuntimeError("still getting 429 after all attempts")
Non-negotiable: if Retry-After is present, use that value, not your own estimate. If missing, double the wait each attempt (1s, 2s, 4s, 8s…) and add a small random jitter to avoid multiple clients retrying at the same instant.
How to verify it's fixed. Monitor for a few hours the rate of 429s in your application logs towards that API: it should drop to zero or near-zero, and x-ratelimit-remaining in responses should no longer reach zero before the window ends.
When to contact your provider's support
If the 429 comes from a CDN/WAF or shared hosting and you don't have access to the rule generating it, open a ticket with: the exact URL that fails, the precise time (with timezone) of at least three rejected requests, the source IP of the requests, and the full text of the 429 response headers (including any header with ray, request-id or similar, which support uses to trace the event in their internal logs). Without this data, support can't distinguish a legitimate block from a false positive.
No Prime Software products mentioned in this article.
Written with the help of artificial intelligence and checked by the editors (EU AI Act, art. 50).