NGINX Error Guide: '503 Service Temporarily Unavailable' — Find the Real Cause
Fix NGINX '503 Service Temporarily Unavailable': tell apart rate-limit rejections, all-upstreams-down, and backend 503s, and restore service right.
- #nginx
- #web-server
- #troubleshooting
- #errors
Stuck on this NGINX error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
A 503 Service Temporarily Unavailable from NGINX means “I can’t serve this right now.” Unlike a 502 (bad response from upstream) or 504 (upstream too slow), a 503 is usually NGINX itself refusing the request — because a rate/connection limit tripped, because every upstream is marked down, or because the backend deliberately returned it:
2026/07/09 11:07:44 [error] 803#803: *20481 limiting requests, excess: 5.320 by zone "api", client: 203.0.113.51, server: api.example.com, request: "GET /v1/search HTTP/1.1", host: "api.example.com"
Because three very different problems all surface as the same status code, the fix depends entirely on why NGINX returned it. The error log is what disambiguates them — the status code alone won’t.
Symptoms
- Clients get
503 Service Temporarily Unavailable, sometimes with aRetry-Afterheader. - The error log shows one of:
limiting requests, excess:(rate limit),limiting connections(conn limit),no live upstreams(all backends down), or nothing from NGINX (the backend sent the 503). - 503s spike with traffic (rate limiting) or with a deploy/health-check failure (upstreams down).
- Some clients or IPs get 503s while others don’t (per-client limits), or everyone does (upstreams down / maintenance).
Common Root Causes
- Rate limiting —
limit_reqrejecting requests above the configured rate, logged aslimiting requests, excess:.limit_req_statusmay be set to 503 (or 429). - Connection limiting —
limit_connrejecting when a client exceeds max simultaneous connections, logged aslimiting connections. - All upstreams down — every server in the
upstreamblock failed its passive health checks; logged asno live upstreams. - Backend-originated 503 — the application itself returned 503 (maintenance mode, readiness failing, circuit breaker); NGINX just passed it through.
- Intentional maintenance mode — a
return 503;toggle in the config serving a maintenance page.
Diagnostic Workflow
The first question is: did NGINX generate the 503, or did the backend? Grep the error log for the distinguishing phrases:
grep -E 'limiting requests|limiting connections|no live upstreams' /var/log/nginx/error.log | tail -20
Correlate 503s in the access log with the upstream status to see if the backend sent them:
# If $upstream_status is 503 the BACKEND returned it; if it's '-' NGINX generated it
grep ' 503 ' /var/log/nginx/access.log | tail
If it’s rate limiting, inspect the zone and decide whether the limit is too tight or an actual flood. A typical limit config:
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /v1/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429; # prefer 429 over 503 so clients can tell them apart
proxy_pass http://backend;
}
}
}
If it’s no live upstreams, check backend health and why NGINX marked them down:
# Are the backends actually up?
curl -s -o /dev/null -w '%{http_code}\n' http://10.0.3.20:8080/healthz
grep 'no live upstreams' /var/log/nginx/error.log | tail
Validate and reload after any config change:
nginx -t && nginx -s reload
Example Root Cause Analysis
A search API began returning 503s during a marketing push. The team first suspected the backend, but a direct curl to each upstream returned healthy 200s. The NGINX error log told the real story: hundreds of limiting requests, excess: ... by zone "api" lines. This was NGINX’s own limit_req doing its job — the campaign had pushed legitimate traffic past the rate=10r/s limit set months earlier for a much smaller load.
Two changes fixed it correctly. First, they raised the zone to a rate appropriate for current traffic and increased burst to absorb spikes with nodelay. Second — and more importantly for clients — they switched limit_req_status from the default 503 to 429 Too Many Requests, so client SDKs could distinguish “you’re being throttled, back off” from “the service is down.” The 503s that remained were now unambiguous, and the on-call runbook no longer confused rate-limit rejections with real outages.
Prevention Best Practices
- Set
limit_req_status 429andlimit_conn_status 429so throttling is distinguishable from genuine unavailability — reserve 503 for real “service down” cases. - Size rate and connection limits to real traffic and revisit them as load grows; a stale limit turns a traffic win into a self-inflicted outage.
- Monitor the error log for
limiting requests,limiting connections, andno live upstreamsseparately — they need different responses. - Configure upstream health so a single flapping backend doesn’t mark the whole pool down; ensure health-check paths are cheap and reliable.
- For intentional maintenance, return 503 WITH a
Retry-Afterheader and make sure monitoring expects it during the window. - Log
$upstream_statusalongside$statusso you can instantly tell an NGINX-generated 503 from a backend-generated one.
Quick Command Reference
# Did NGINX or the backend generate the 503?
grep -E 'limiting requests|limiting connections|no live upstreams' /var/log/nginx/error.log | tail
# Check backend health directly
curl -s -o /dev/null -w '%{http_code}\n' http://UPSTREAM_IP:PORT/healthz
# Count 503s over time in the access log
grep ' 503 ' /var/log/nginx/access.log | wc -l
# Validate and reload after adjusting limits
nginx -t && nginx -s reload
Conclusion
A 503 Service Temporarily Unavailable from NGINX is a symptom with at least three distinct causes — rate/connection limiting, all upstreams down, or a backend-originated 503 — and the status code alone won’t tell you which. Let the error log disambiguate: limiting requests/limiting connections means your own limits, no live upstreams means dead backends, and silence from NGINX means the app sent it. Fix the actual cause, and use 429 for throttling so future 503s always mean “genuinely unavailable.”
Fixed it? Get 500 NGINX & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.