What a 503 means

502 and 504 describe a proxy that tried to get an answer and failed. 503 is different in kind: something decided not to serve the request right now. RFC 9110 defines it as the server being “currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay.”

In real deployments that decision comes from one of a handful of places:

  • Maintenance mode, switched on by a person or a deploy script.
  • Rate limiting or overload protection, refusing you because you (or your IP) sent too much.
  • No backend available: the load balancer or ingress has nothing healthy to send the request to.
  • A process manager or app pool that is stopped or restarting.

Because it’s a decision, a 503 is usually fast. A request that hangs for 60 seconds and then fails is a timeout, not a 503, with one family of exceptions covered below.

The response may carry a Retry-After header, either a number of seconds or a date. It’s optional, but when present it’s the server telling you how long to wait.

If you’re visiting the site

If everyone gets the error, the site is in maintenance or overloaded. Wait and try again; there’s nothing to fix on your side.

If only you get it, rate limiting is the likely reason. Limits are counted per IP address, so a busy VPN exit, a shared office connection, or a mobile carrier that puts many customers behind one address can push you over a limit you didn’t cause. Wait a minute, turn off the VPN, or switch from Wi-Fi to mobile data. Repeatedly reloading makes rate limiting worse, because every reload counts.

Find out who said it

The status code alone tells you less than you’d like here, because different software means different things by 503. Look at the headers and the body:

curl -sv -o /dev/null https://example.com/ 2>&1 | grep -iE '^< (HTTP|server|retry-after|via|x-cache|x-served-by|cf-ray)'

Common signatures, and what each one means:

What you seeWho sent itUsual meaning
nginx page “503 Service Temporarily Unavailable”nginxlimit_req/limit_conn rejected you, or a deliberate return 503 for maintenance
“Briefly unavailable for scheduled maintenance. Check back in a minute.”WordPressAn update is running, or one was interrupted
Server: awselb/2.0AWS ALBThe target group has no registered targets, or a rule returns a fixed 503
“No server is available to handle this request.”HAProxyEvery server in the backend is down or in maintenance
“Error 503 Backend fetch failed” and “Guru Meditation”VarnishThe backend failed or timed out
“Error 503” with “first byte timeout” or “backend read error”FastlyThe origin timed out or failed
“The server is temporarily unable to service your request due to maintenance downtime or capacity problems”ApacheOften mod_proxy that couldn’t connect to its backend
“HTTP Error 503. The service is unavailable.” with Server: Microsoft-HTTPAPI/2.0Windows IIS (HTTP.sys)The application pool is stopped

Note the Varnish, Fastly and Apache rows. Those products report a backend that is down or too slow as 503, where nginx would say 502 or 504. So a 503 from a Varnish-based cache is really “my backend failed”, and you debug it like a 502 or a 504, starting at the backend.

The HTTP check shows the status code and headers, including Server and Retry-After, as seen from outside your network.

The usual causes, and fixes

Maintenance mode that was left on

The most embarrassing and most common cause. A deploy script switched maintenance on and failed before switching it off.

  • WordPress writes a .maintenance file to the site root during core, plugin and theme updates. If the update is interrupted (a timeout, a closed browser tab), the file stays and the site keeps answering 503 with the “Briefly unavailable” message. Delete .maintenance from the WordPress root, then check that the interrupted update actually completed.
  • Laravel returns 503 after php artisan down; php artisan up reverses it.
  • Custom nginx switches often check for a flag file. Look for it with sudo nginx -T | grep -n -B2 -A4 'return 503'.

If you do want a maintenance page, make it a proper one. Return 503, not 200, so search engines don’t index the maintenance page as your content, and add Retry-After:

error_page 503 @maintenance;

location @maintenance {
    root /var/www/maintenance;
    add_header Retry-After 600 always;
    rewrite ^ /index.html break;
}

location / {
    if (-f /etc/nginx/maintenance.flag) {
        return 503;
    }
    proxy_pass http://app;
}

always matters: without it nginx only adds headers to successful and redirect responses, not to a 503. Then sudo touch /etc/nginx/maintenance.flag turns maintenance on and deleting the file turns it off, no reload needed.

Search engines treat a 503 as temporary and come back later. That holds for hours, not weeks: if the site answers 503 for days, expect pages to start dropping out of the index.

Rate limiting that is too tight, or counts the wrong IP

nginx’s limit_req and limit_conn reject with 503 by default. In the error log the rejection looks something like:

2026/10/07 11:20:05 [error] 2114#2114: *10422 limiting requests, excess: 20.240 by zone "per_ip", client: 203.0.113.50, server: example.com, request: "GET /api/search?q=x HTTP/1.1", host: "example.com"

Two fixes, both worth doing.

Return 429 Too Many Requests instead, so clients, monitoring and you can tell rate limiting apart from an outage:

limit_req_status 429;
limit_conn_status 429;

Then check that the limit counts the right address. If nginx sits behind a load balancer or CDN and keys its zone on $binary_remote_addr, every visitor shares the load balancer’s few IP addresses, and one busy minute rate-limits everyone. Configure the realip module (set_real_ip_from for your proxy’s ranges, real_ip_header for the header it sets) so $remote_addr becomes the visitor’s address. Only trust that header from your proxy’s addresses; trusting it from everyone lets any client pick its own IP.

Also check whether burst is set. Without it, limit_req allows no excess at all, and a browser loading a page with twenty assets can trip a limit meant for scripts.

No healthy backends

The load balancer or ingress is fine; there is nothing behind it to send traffic to.

  • AWS ALB returns 503 when the target group has no registered targets. Note what it doesn’t do: if targets are registered but all fail health checks, the ALB “fails open” and sends traffic to them anyway, so you see their errors (often 502) rather than 503. A 503 from an ALB therefore usually means an empty target group, often an Auto Scaling group scaled to zero or a deploy that deregistered everything at once.
  • Kubernetes ingress-nginx answers 503 when the Service has no ready endpoints. kubectl get endpoints <service> showing nothing means either no Pods are passing their readiness probe or the Service selector doesn’t match the Pod labels. A readiness probe that checks a dependency (the database) can take every Pod out at once when that dependency hiccups; keep readiness about the Pod itself.
  • HAProxy shows “No server is available” when every server is marked down. show stat on the stats socket tells you which health check failed.

Overload and slow scale-out

Traffic grew faster than capacity, and something is shedding the excess as 503: a concurrency limit, a queue that is full, an app pool protecting itself. Autoscaling helps with sustained growth but not with a sudden spike, because new instances take minutes to boot and pass health checks. During that gap the existing ones refuse work.

Short-term fixes are the obvious ones: more instances, or a higher minimum. Longer term, cache what can be cached at the edge so a spike doesn’t reach the application at all, and make sure the expensive routes aren’t the ones a spike hits.

A stopped process

On Windows, IIS returns its HTTP.sys 503 when an application pool is stopped, often by Rapid-Fail Protection after the app crashed several times in a short window. The Windows event log (System and Application) records why; fix the crash, then start the pool. On Linux, the closest equivalents usually come out as 502 instead, because nginx reports a dead backend that way. If you see 503 through Apache, check whether the backend ProxyPass points to is running: Apache’s mod_proxy logs something like AH00957: http: attempt to connect to 127.0.0.1:8080 (127.0.0.1) failed and answers 503.

After the fix

  • Re-check from outside with the HTTP check, not just from the server. Rate limits and per-network blocks look fine from the inside.
  • Confirm the Retry-After header on your maintenance page with curl -sI.
  • If the 503 came from a rate limit, look at the log over a normal day: a limit that rejects real visitors every afternoon needs a higher rate or burst, not a one-off clear.
  • The port check confirms the public port answers at all; a 503 needs an open port to be delivered, so if the port is closed you’re looking at a different problem.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.