# HTTP 503 Service Unavailable: Causes, Fixes and Retry-After

> Fix HTTP 503 Service Unavailable: nginx no live upstreams, Kubernetes endpoints, ALB healthy hosts, Cloudflare, and a maintenance page with Retry-After.

Source: https://howhttpworks.com/status-codes/503
Last reviewed: 2026-10-05

> **TL;DR:** HTTP 503 Service Unavailable means the server cannot handle your request right now, often because of overload or maintenance. Find who answered, check backend health, and if you serve 503 on purpose, send `Retry-After`.

## What it means

RFC 9110 section 15.6.4: the server is currently unable to handle the request due to temporary overload or scheduled maintenance. The key difference from [500](https://howhttpworks.com/status-codes/500) is that 503 says retrying later is expected to work. Some proxies also use it when no backend is available. An ALB with all targets unhealthy can fail open and route to them.

Server responses and client messages below are illustrative.

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

## What you see in your client

These messages assume the response reason is `Service Unavailable`; a server can use different wording.

- **Axios:** `AxiosError: Request failed with status code 503`. With the default `validateStatus`, Axios rejects this response. Inspect `error.response.status`, `error.response.headers` and `error.response.data` before changing the request. [Axios source](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **fetch:** the promise resolves with `response.status === 503` and `response.ok === false`. Check `ok` yourself; a `catch` block alone misses HTTP error responses. [Fetch behaviour](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch).
- **Python requests:** `requests.exceptions.HTTPError: 503 Server Error: Service Unavailable for url: https://example.com/api`. This appears when you call `response.raise_for_status()`. Save the response body and headers before raising. [Requests source](https://github.com/psf/requests/blob/main/src/requests/models.py).
- **curl -f:** `curl: (22) The requested URL returned error: 503`. Use `curl -sS -D - https://example.com/api` while diagnosing so you retain the error body. [curl source](https://github.com/curl/curl/blob/master/lib/http.c).
- **.NET:** `System.Net.Http.HttpRequestException: Response status code does not indicate success: 503 (Service Unavailable).` This is the English message from `EnsureSuccessStatusCode()` with that reason phrase. Inspect `StatusCode` and read the body before calling it. [Runtime source](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs), [message resource](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring WebClient:** `org.springframework.web.reactive.function.client.WebClientResponseException$ServiceUnavailable: 503 Service Unavailable from GET https://example.com/api`. `retrieve()` uses this subclass by default; read `getResponseBodyAsString()`. RestTemplate uses `HttpServerErrorException.ServiceUnavailable` for this 5xx status. [WebClient source](https://github.com/spring-projects/spring-framework/blob/main/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java), [RestTemplate handler](https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java).

## Who sent it?

| Signal | Layer and typical cause |
| --- | --- |
| `Server: nginx`, page "503 Service Temporarily Unavailable" | nginx: `limit_req`/`limit_conn` rejection, or a deliberate `return 503` |
| `Server: awselb/2.0` and an empty or minimal body | ALB: no registered targets, or all registered targets are `unused` |
| ingress-nginx page, log `Service "ns/name" does not have any active Endpoint` | Kubernetes: no Ready pods behind the Service |
| `Server: cloudflare` with an origin error page, or Cloudflare branded page | Origin sent 503, or Cloudflare generated a data-center connectivity error |
| `X-Cache: Error from cloudfront`, "The request could not be satisfied" | Origin overload, CloudFront capacity or an edge-function limit/error |
| App log shows pool exhausted, queue full | The application itself is shedding load |

nginx request-limit log (sample request identifiers):

```text
[error] 29#29: *513 limiting requests, excess: 10.300 by zone "api", client: 203.0.113.9
```

## Common causes by stack

- **nginx:** `limit_req_status` and `limit_conn_status` both default to 503. A burst beyond `limit_req`'s allowance is a rejection; inspect `limiting requests` in the error log. For deliberate per-client throttling, configure 429. [Request limits](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html), [connection limits](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html).
- **Laravel:** `php artisan down --retry=60` activates maintenance and supplies `Retry-After`. Run `php artisan up` when deployment is complete; check that every serving instance has left maintenance. [Laravel maintenance mode](https://laravel.com/docs/12.x/configuration#maintenance-mode).
- **ASP.NET Core:** rate-limiting middleware's `RejectionStatusCode` defaults to 503. If the rejection body says too many concurrent requests, inspect the limiter policy and queue before adding retries; set 429 for a per-client quota. [RateLimiterOptions](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/RateLimiting/src/RateLimiterOptions.cs).
- **Kubernetes ingress-nginx:** an empty backend produces 503; the controller warns `Service "namespace/name" does not have any active Endpoint.` Check selectors and readiness, then inspect EndpointSlices. An existing endpoint with a refused port points toward 502 instead. [Controller source](https://github.com/kubernetes/ingress-nginx/blob/main/internal/ingress/controller/controller.go), [Service debugging](https://kubernetes.io/docs/tasks/debug/debug-application/debug-service/).
- **AWS ALB:** no registered targets or all targets `unused` produces ALB-generated 503. All targets merely unhealthy is different: ALB can fail open. [ALB errors](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html#http-503-issues), [health-check behaviour](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html).
- **Cloudflare/CloudFront:** identify the error body before changing origin capacity. Cloudflare documents its own 503 connectivity errors; CloudFront lists origin overload, edge capacity and function execution limits. [Cloudflare 503](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-503/), [CloudFront 503](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/http-503-service-unavailable.html).

## Fix it

1. Confirm the backends are up and passing health checks: `curl -sv http://BACKEND:PORT/health` from the proxy host.
2. nginx `limit_req` / `limit_conn`: both reject with 503 by default. Set `limit_req_status 429;` and `limit_conn_status 429;` so clients see the right code (see [429](https://howhttpworks.com/status-codes/429)).
3. Kubernetes: inspect `kubectl get endpointslices -l kubernetes.io/service-name=my-svc -o yaml`, then `kubectl describe pod POD`. Match Ready endpoints to the Service selector and port. Check rollout capacity and the termination grace period when failures coincide with deploys.
4. ALB: in the target group, check "healthy host count" and the health check path, port and success codes; a 503 from the ALB itself with no targets registered is a configuration issue, not load.
5. Overload: look at CPU, memory, database connection pool size and worker counts (gunicorn `--workers`, PHP-FPM `pm.max_children`); autoscale or shed load deliberately.
6. Cloudflare in front: if the 503 body is your origin's, fix the origin; if it contains `cloudflare` or `cloudflare-nginx`, use Cloudflare's 503 troubleshooting path. For distinct origin connection failures, inspect the 52x codes (see [520](https://howhttpworks.com/status-codes/520) to [524](https://howhttpworks.com/status-codes/524)).

## Return 503 on purpose: maintenance mode

Send `Retry-After`, mark the response uncacheable, and keep your health endpoint working. Create `/var/www/maintenance/index.html` before enabling this nginx configuration; its named error location preserves the 503 status. Retry-After is either seconds or an HTTP date:

```nginx
server {
    error_page 503 @maintenance;

    location / {
        return 503;
    }

    location = /health {
        return 200 "ok\n";
    }

    location @maintenance {
        add_header Retry-After 3600 always;
        add_header Cache-Control "no-store" always;
        root /var/www/maintenance;
        rewrite ^ /index.html break;
    }
}
```

```javascript
// Express: shed load or enter maintenance with a correct Retry-After
app.use((req, res, next) => {
  if (!maintenanceMode) return next()
  res.set({ 'Retry-After': '3600', 'Cache-Control': 'no-store' })
  res.status(503).type('text/html').send('<h1>Back soon</h1>')
})
```

Clients must accept both forms of the header:

```javascript
function retryAfterMs(res) {
  const v = res.headers.get('retry-after')
  if (!v) return null
  const n = Number(v)
  if (Number.isFinite(n)) return n * 1000
  const t = Date.parse(v)
  return Number.isNaN(t) ? null : Math.max(0, t - Date.now())
}
```

## SEO during maintenance

Serve temporary maintenance at the original URLs with 503 and `Retry-After`. Google reduces crawling for 5xx responses, ignores their response content and initially preserves indexed URLs; persistent errors eventually remove them. Returning 200 makes the maintenance body eligible for indexing. Restore normal responses promptly rather than redirecting every URL to one maintenance page. [Google's HTTP guidance](https://developers.google.com/search/docs/crawling-indexing/http-network-errors).

## Retrying a concurrency rejection

For a 503 body containing `"too many concurrent requests"`, reduce parallel requests and queue work on the client. Wait for `Retry-After` when supplied; otherwise use bounded exponential backoff with jitter. Each retry consumes capacity too. For a POST that changes state, check its result or use the service's idempotency mechanism before repeating it.

## Reproduce

```bash
curl -sI https://example.com/ | grep -iE '^(HTTP|retry-after|server|via|x-cache|cf-ray)'
```

## Related codes

- [500 Internal Server Error](https://howhttpworks.com/status-codes/500): the app failed unexpectedly, not unavailable.
- [502 Bad Gateway](https://howhttpworks.com/status-codes/502): the proxy got an invalid or refused response from the upstream.
- [504 Gateway Timeout](https://howhttpworks.com/status-codes/504): a gateway timed out connecting or waiting for an upstream.
- [429 Too Many Requests](https://howhttpworks.com/status-codes/429): you specifically are being throttled.

See also the comparison [502 vs 503 vs 504](https://howhttpworks.com/compare/502-vs-503-vs-504): how to tell the three gateway errors apart from the nginx error log and Cloudflare codes.
