Skip to content
DevOps AI ToolKit
All guides
AI for NGINX By James Joyner IV · · 9 min read Last reviewed Aug 2026

NGINX Error Guide: '503 Service Temporarily Unavailable' — Find the Real Cause

Quick answer

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
Free toolkit

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 a Retry-After header.
  • 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 limitinglimit_req rejecting requests above the configured rate, logged as limiting requests, excess:. limit_req_status may be set to 503 (or 429).
  • Connection limitinglimit_conn rejecting when a client exceeds max simultaneous connections, logged as limiting connections.
  • All upstreams down — every server in the upstream block failed its passive health checks; logged as no 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 429 and limit_conn_status 429 so 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, and no live upstreams separately — 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-After header and make sure monitoring expects it during the window.
  • Log $upstream_status alongside $status so 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.

Free download · 368-page PDF

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.

Free download · 368-page PDF

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.