# nginx 504 Gateway Timeout: Fix Upstream Timed Out (110)

> Fix nginx 504 Gateway Time-out and 'upstream timed out (110)': proxy_read_timeout, fastcgi_read_timeout, ALB idle timeout, and Cloudflare 524 compared.

Source: https://howhttpworks.com/debug/nginx-504-gateway-timeout
Last reviewed: 2026-10-04

Error messages this page covers:
- `504 Gateway Time-out`
- `upstream timed out (110: Connection timed out) while reading response header from upstream`
- `upstream timed out (110: Connection timed out) while connecting to upstream`
- `upstream timed out (110: Connection timed out) while sending request to upstream`

> **TL;DR:** nginx forwarded the request to the upstream and gave up waiting (all three `proxy_*_timeout` directives default to 60s). The `upstream timed out (110: Connection timed out)` error-log line names the phase: connecting, sending or reading. Fix the slow upstream first; raise `proxy_read_timeout` (or `fastcgi_read_timeout`) only for bounded slow work, and raise every timeout in front of nginx too.

## What it means

A 504 is the gateway telling the client that it did not get a timely response from the server behind it. In nginx the browser sees:

```http
HTTP/1.1 504 Gateway Time-out
Server: nginx/1.27.0
Content-Type: text/html
```

```text
2026/10/04 10:03:17 [error] 31#31: *482 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.10, server: example.com, request: "GET /report HTTP/1.1", upstream: "http://127.0.0.1:3000/report", host: "example.com"
```

The tail of the `upstream timed out` line identifies the phase, and each phase has its own directive. All three default to 60 seconds:

| Log says | Directive | What it measures |
| --- | --- | --- |
| `while connecting to upstream` | `proxy_connect_timeout` | Time to establish the TCP connection. Raising it rarely helps; an upstream that does not accept a connection in a few seconds is down or unreachable. |
| `while sending request to upstream` | `proxy_send_timeout` | Maximum gap between two writes of the request to the upstream. Matters for large uploads to a slow upstream. |
| `while reading response header from upstream` | `proxy_read_timeout` | Maximum gap between two reads from the upstream. Usually the culprit: the app is still working and has sent nothing. |

For PHP-FPM, uWSGI, and gRPC upstreams the equivalents are `fastcgi_*_timeout`, `uwsgi_*_timeout` and `grpc_*_timeout`.

## Who sent it?

Not every 504 comes from nginx. Look at the failing response:

- `Server: nginx` and the page title `504 Gateway Time-out`: nginx on your host or the ingress controller. Confirm with the `upstream timed out` log line with the matching timestamp.
- `Server: awselb/2.0`: an AWS Application Load Balancer. It returns 504 when a target does not respond before the idle timeout (60 seconds by default) or when it cannot establish a connection. It logs the reason in access logs under `error_reason`.
- `X-Cache: Error from cloudfront` and `Via: ... (CloudFront)`: CloudFront gave up on the origin (origin response timeout, 30 seconds by default).
- `Server: cloudflare` with a Cloudflare-branded page: look at the number. "Error 504" is Cloudflare relaying a gateway timeout; "Error 524: A timeout occurred" means the origin accepted the connection but did not answer in time (125 seconds by default). "Error 522" means the connection to the origin timed out.
- JSON or framework-styled body: an application server or API gateway is generating the 504 itself.

Compare timings. A 504 that always arrives after about 60 seconds points at a default nginx or ALB timeout. One that arrives after 30 seconds points at CloudFront, many API gateways or a Gunicorn worker timeout. One that arrives at about 125 seconds points at Cloudflare (older guides say 100; Cloudflare's current docs say 125).

## Fix it, in order of likelihood

1. Find out why the upstream is slow. Check the upstream's own logs for the same request, and time it directly: `curl -w '%{time_starttransfer}\n' -o /dev/null -s http://127.0.0.1:3000/report`. If the upstream is healthy but nginx times out, you have a configuration problem; if the upstream is slow, no nginx setting is the real fix.
2. Look at the phase in the error log, then raise the matching timeout in the narrowest `location`.
3. Raise the limits of every proxy in front: ALB idle timeout, CloudFront origin response timeout, Cloudflare plan limits.
4. Raise the limits of the application server so it does not kill the worker first: Gunicorn `--timeout` (30 seconds by default), PHP-FPM `request_terminate_timeout`, PHP `max_execution_time`.
5. For work that takes more than a minute or so, switch to an asynchronous API.

### nginx

```nginx
location /report {
    proxy_pass http://app;

    proxy_connect_timeout 5s;     # fail fast if the upstream is down
    proxy_send_timeout    60s;
    proxy_read_timeout    180s;   # only for this slow endpoint, not the whole server

    # Do not retry a slow request on another upstream; it doubles the load and the wait
    proxy_next_upstream off;
}
```

`proxy_next_upstream` defaults to `error timeout`. With several upstreams, a timeout is retried on the next one, so a single slow request can occupy every backend and the client waits for the sum of the timeouts. For non-idempotent methods nginx does not retry by default unless you add `non_idempotent`.

### PHP-FPM

```nginx
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_read_timeout 180s;     # default 60s
}
```

```ini
; php-fpm pool config
request_terminate_timeout = 180s

; php.ini
max_execution_time = 180
```

If PHP is terminated before nginx times out, the symptom flips to a 502 (`upstream prematurely closed connection`). Keep nginx's timeout slightly higher than PHP's.

### Kubernetes ingress-nginx

Annotations take plain seconds as strings. The ConfigMap defaults are 5 for connect and 60 for send and read:

```yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "5"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "180"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "180"
```

### AWS Application Load Balancer

Set the idle timeout on the load balancer attributes (default 60 seconds). It limits the time a connection may stay idle in either direction, so a slow request with no bytes flowing hits it exactly like nginx's read timeout does:

```bash
aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn "$ALB_ARN" \
  --attributes Key=idle_timeout.timeout_seconds,Value=180
```

If nginx sits behind the ALB, make nginx's `proxy_read_timeout` at least as long, otherwise nginx is the first to time out and you will see nginx-style 504s instead.

### Cloudflare

Cloudflare's Proxy Read Timeout is 125 seconds by default ([error 524](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/)). Only Enterprise customers can raise it, up to 6,000 seconds. Beyond the limit the visitor gets 524 regardless of your origin settings. If a request genuinely needs to be longer, take that route out of the proxied path (a grey-clouded DNS-only hostname for the API), or restructure the work into a job: respond `202 Accepted` with a `Location` of a status resource and let the client poll.

### The 202 pattern

```http
POST /reports HTTP/1.1

HTTP/1.1 202 Accepted
Location: /reports/8f3a
Retry-After: 5
```

```http
GET /reports/8f3a HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json

{"status":"running"}
```

## Reproduce and verify

Create a deliberately slow endpoint or use a sleep in a test upstream, then time the request through each layer. Compare the elapsed times to see which one cuts the request:

```bash
# Through the public URL
curl -sS -o /dev/null -w 'status=%{http_code} total=%{time_total}s\n' https://example.com/report

# Straight to the origin, skipping the CDN
curl -sS -o /dev/null -w 'status=%{http_code} total=%{time_total}s\n' \
  -H 'Host: example.com' http://ORIGIN_IP/report

# Straight to the app, skipping nginx
curl -sS -o /dev/null -w 'status=%{http_code} total=%{time_total}s\n' http://127.0.0.1:3000/report
```

A 504 at `total=60.0xx` from the first two and a success from the third means the nginx or ALB 60 second default is cutting you off. Watch the error log live while you retry with `tail -f /var/log/nginx/error.log`.

## Related

- [504 Gateway Timeout](https://howhttpworks.com/status-codes/504) describes the status and how it differs from a client-side timeout.
- [524 A Timeout Occurred](https://howhttpworks.com/status-codes/524) is Cloudflare's version, with a fixed 125 second default budget.
- [502 Bad Gateway](https://howhttpworks.com/debug/nginx-502-bad-gateway) is the other half of nginx upstream failures: the upstream answered badly or closed the connection.
- [503 Service Unavailable](https://howhttpworks.com/status-codes/503) is what an overloaded or draining upstream should answer instead of hanging.
- [ERR_INCOMPLETE_CHUNKED_ENCODING](https://howhttpworks.com/debug/err-incomplete-chunked-encoding): the timeout hits after the response has started, so the browser sees a cut-off body instead of a 504.
