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.”
Frequently Asked Questions
How do I tell which cause of 503 I have? Read the NGINX error log. limiting requests/limiting connections means your own limit_req/limit_conn rejected it; no live upstreams means every backend is down or failing health checks; no NGINX line at all means the upstream application returned the 503 itself.
Is a 503 the same as a 502? No. 502 Bad Gateway means the upstream returned an invalid or broken response; 503 Service Temporarily Unavailable means the service is reachable but declining requests — overloaded, rate-limited, or with no available upstream.
Should I use 429 instead of 503 for rate limiting? Yes. Return 429 Too Many Requests (set limit_req_status 429;) for throttling, so a genuine 503 always means the service is truly unavailable rather than deliberately shedding load.
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.
Stuck on this? Start guided troubleshooting
Open an interactive diagnostic session with this error already loaded. Work a step-by-step plan, record what each check returns, land on a root cause, and export a clean incident summary — no account needed to start.
Did this fix your issue?
Solved it a different way?
Share the fix that worked for you — reviewed, then published to help the next engineer.
That looks like it may contain a secret (key, token, password, or connection string). Please remove it — a note with a detected secret can’t be published.
Thanks — that helps. Published notes appear after a quick review.
Trending errors this week
The error guides other engineers are actually reading right now.
- 1mount: wrong fs type, bad option, bad superblock
- 2mount: wrong fs type, bad option, bad superblock
- 3Docker 'failed to set up container networking': Fix the Bridge and IP Pool
- 4Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block
- 5Docker 'failed to create shim task': How to Fix the containerd Runtime Error
- 6Too many levels of symbolic links
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.