Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Troubleshooting

HTTP 503: What Is Actually Broken When the Service Is Unavailable

Sean

Platform Writer

Aug 13, 2026
8 min read

A 503 almost always means something answered the request but the thing meant to handle it did not. A reverse proxy is up and reachable; the application behind it is dead, restarting, saturated, or failing its health check. That is the key diagnostic fact — it separates 503 from a connection refused (nothing listening at all) and from a 500 (the application ran and threw).

HTTP 503: What Is Actually Broken When the Service Is Unavailable

The status code is generic by design, so the useful work is narrowing down which of five causes you have. They present almost identically in a browser and quite differently in the logs.

Table of contents

Where the 503 is generated tells you most of it

First establish who produced the response, because that halves the search space immediately.

curl -sS -D - -o /dev/null https://example.com/

# Look at the Server header and any vendor-specific ones
# Server: nginx          -> your proxy generated it
# Server: cloudflare     -> the CDN generated it; your origin may be fine
# Server: gunicorn       -> your app generated it deliberately

A 503 from your own reverse proxy means the upstream is unreachable. A 503 from a CDN often means the CDN could not reach your origin. A 503 from the application itself is usually deliberate — maintenance mode, or a health endpoint reporting a dependency is down.

Then check whether the application is actually listening:

sudo ss -tlnp | grep -E ':(3000|8000|8080)'
sudo systemctl status myapp
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/

If the direct request to the application works and the proxied one 503s, the problem is between them — configuration, socket permissions, or SELinux. If the direct request fails too, the application is the problem.

Cause one: the application is not running

The most common cause and the easiest to confirm. The process crashed, failed to start after a deploy, or was killed.

journalctl -u myapp -n 100 --no-pager
journalctl -u myapp -p err -b

# Killed by the OOM killer?
sudo dmesg -T | grep -i 'killed process'
journalctl -k | grep -i 'out of memory'

The OOM killer is worth checking early because it produces a confusing signature: the application vanishes with no error in its own logs, because it did not get to write one. dmesg is the only place the reason appears.

A repeating crash-restart cycle produces intermittent 503s — some requests hit a live process, some hit the gap. If the 503s are intermittent rather than constant, check the restart count: systemctl show myapp -p NRestarts.

Cause two: the worker or connection pool is saturated

The application is running fine and every worker is busy, so new requests queue and then get rejected. This is the cause that looks like a mystery because everything appears healthy.

The usual root is slow database queries. Each request holds a worker while waiting; workers run out; the proxy gets no free upstream and returns 503.

-- Postgres: what is running right now
SELECT pid, now() - query_start AS duration, state, left(query, 80)
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC
LIMIT 20;

-- Are connections themselves the limit?
SELECT count(*), setting::int AS max_connections
FROM pg_stat_activity, pg_settings
WHERE pg_settings.name = 'max_connections'
GROUP BY setting;

Raising the worker count is the reflex and it is usually wrong — more workers competing for the same exhausted database connections makes it worse. Fix the slow query, add the missing index, or add a connection pooler. The worker count is the symptom’s location, not its cause.

Cause three: nginx cannot reach the upstream

Configuration and permissions, and the error log says exactly which.

sudo tail -50 /var/log/nginx/error.log
  • connect() failed (111: Connection refused) — nothing is listening on that address and port. Check the app is up and bound where nginx expects.
  • no live upstreams while connecting to upstream — every server in the upstream block has been marked down by failed health checks.
  • upstream timed out (110: Connection timed out) — the app accepted the connection but did not respond in time. This is usually a 504, but appears as 503 in some configurations.
  • connect() to unix:/run/app.sock failed (13: Permission denied) — socket permissions. The nginx user cannot open the socket.

The bind-address mistake deserves a mention: an application bound to 127.0.0.1 is unreachable from nginx in a different container or namespace. Binding to 0.0.0.0 fixes it, and then the port must be firewalled rather than left open.

Cause four: deliberate, and cause five: rate limiting

Not every 503 is a fault. Maintenance mode returns 503 on purpose, and it is the correct code — it tells search engines the outage is temporary and not to drop the page from the index.

HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: text/html

Retry-After takes seconds or an HTTP date and is the difference between a well-behaved outage and one that gets hammered by retrying clients. Include it whenever you return 503 on purpose.

Rate limiters also return 503 in some configurations, though 429 is the more correct code. If your 503s correlate with traffic spikes and stop when traffic drops, check the limiter before assuming a capacity problem.

Making 503 rare rather than merely diagnosable

Most 503s reduce to one of two structural gaps: there was one instance, so its death was an outage; or a bad deploy replaced a working version and nothing brought the old one back.

  • A real health check. One that touches the database rather than returning 200 unconditionally, so an instance that cannot serve is removed rather than kept in rotation.
  • More than one instance. A single instance means every restart is downtime.
  • Rolling deploys. New version healthy before the old one stops.
  • Fast rollback. The fastest fix for a bad deploy is the previous deploy, not a hotfix written under pressure.
  • Headroom. Autoscaling between a floor and a ceiling absorbs the spikes that saturate a fixed worker pool.

That last pair is what makes 503 an incident rather than a blip. On RunxBuild, autoscaling runs between plans you choose — scaling up at 80% CPU and back down at 20% — and every deploy is retained, so rolling back to the previous version is one action rather than a hurried redeploy while the site is down.

How this fits the rest of the stack

Find out who generated the 503 first — proxy, CDN, or application — then check whether the app is listening, whether it was OOM-killed, whether the workers are saturated by slow queries, and what nginx’s error log says. Add Retry-After when you return 503 deliberately.

Structurally, 503s get rare when there is more than one instance, a health check that means something, and a rollback that takes seconds. If you are working out what that shape costs, the RunxBuild hosting calculator itemises the service, the database, the storage, and the bandwidth rather than quoting a single number.

Useful related references:

FAQ

What does HTTP 503 mean?

Service Unavailable — the server received the request but cannot handle it right now. In practice a reverse proxy is up while the application behind it is down, restarting, saturated, or failing its health check. It is meant to signal a temporary condition, which is why maintenance pages use it.

What is the difference between a 500 and a 503?

A 500 means the application ran and raised an unhandled error, so the fix is in your code and the stack trace is in the application log. A 503 means the application never handled the request at all — it is not running, not reachable, or has no capacity — so you look at process state and proxy logs instead.

Why do I get 503 errors only under load?

The worker or connection pool is exhausted. Requests occupy workers while waiting on something slow, usually database queries, until no worker is free and the proxy has nothing to route to. Adding workers rarely helps because they contend for the same database connections; fix the slow query or add a pooler.

Should I use 503 for a maintenance page?

Yes. It correctly signals a temporary condition, so search engines keep the page indexed rather than treating it as removed. Include a Retry-After header with a duration in seconds so clients and crawlers know when to come back instead of retrying immediately.

How do I stop 503 errors happening at all?

Run more than one instance so a single restart is not an outage, give the health check something real to test such as a database ping, use rolling deploys so the new version is healthy before the old stops, and keep a fast rollback. Headroom through autoscaling covers the load-driven cases.

#503#http status code#service unavailable#nginx#debugging