What the message looks like
HTTP/2 429
retry-after: 30
203.0.113.5 - - [29/Oct/2026:10:22:41 +0000] "GET /api/items HTTP/2.0" 429 169 "-" "python-requests"Common causes
- A client or script sends requests too fast, without back-off or retries, and exceeds a quota.
- A rate limit in nginx (
limit_reqwithlimit_req_status 429), fail2ban, a WAF or the CDN is stricter than the real traffic. - Many users share one IP (office, mobile network, proxy) and are counted as one client, or the real visitor IP is not restored behind a proxy.
- Bots, crawlers or a vulnerability scan hammer the site.
- A remote API (the 429 comes from a third party) has used up its quota.
Check
- Who gets 429 in the access log (status is field 9 in the default format)?
awk '$9 == 429 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head - Which limits are configured?
nginx -T 2>/dev/null | grep -nE 'limit_req|limit_conn|limit_req_status' - What does the response say about the limit?
curl -sI https://example.com/ | grep -iE '^(HTTP|retry-after|x-ratelimit)' - Is the client IP the real visitor IP in the log?
nginx -T 2>/dev/null | grep -nE 'real_ip|set_real_ip_from'
Fix
- Identify the layer first: the access log (own server), the response headers (CDN or API) and the fail2ban status tell you who answers.
- For your own limit, adjust it: for example
limit_req zone=perip burst=20 nodelay;with a rate that matches normal use. - Restore the real client IP behind a proxy (
real_ip_header,set_real_ip_from) so users are not counted together. - Let known good clients skip the limit with
geoandmapor an allow list, and block abusive bots separately. - As a client: honor
Retry-After, use exponential back-off, cache responses and batch requests.
FAQ
Does nginx limit_req return 429 by default?
No, the default status is 503. Set limit_req_status 429; if you want the standard code.
Should I retry immediately?
No. Wait at least for the time in Retry-After if present, and increase the delay on each retry, otherwise you stay limited.