# 504 Gateway Timeout: nginx, ALB and Cloudflare Fixes

> Fix 504 Gateway Timeout: nginx proxy_read_timeout (60s default), ALB 60s idle, API Gateway 29s, Cloudflare 524 at 125s, with error-log strings and curl timing.

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

> **TL;DR:** HTTP 504 Gateway Timeout means a proxy waited too long for an upstream server. Check the layer that answered, find the slow dependency, and then either speed the request up or raise the timeout in every layer in front of it, because the shortest timeout wins. nginx read and ALB idle timeouts default to 60 seconds; API Gateway limits depend on the API type.

## What it means

RFC 9110 section 15.6.5: a server acting as a gateway or proxy did not receive a timely response from an upstream server it needed to access. The timeout can happen while connecting or waiting for response data. ALB documents a 504 when connection establishment exceeds its 10-second timeout. A refused or reset connection commonly produces [502](https://howhttpworks.com/status-codes/502).

Server responses and client messages below are illustrative.

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

nginx writes "504 Gateway Time-out" with a hyphen, and logs:

```text
[error] 29#29: *731 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.9, server: example.com, request: "GET /report HTTP/1.1", upstream: "http://127.0.0.1:8000/report"
```

## What you see in your client

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

- **Axios:** `AxiosError: Request failed with status code 504`. 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 === 504` 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: 504 Server Error: Gateway Timeout 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: 504`. 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: 504 (Gateway Timeout).` 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$GatewayTimeout: 504 Gateway Timeout from GET https://example.com/api`. `retrieve()` uses this subclass by default; read `getResponseBodyAsString()`. RestTemplate uses `HttpServerErrorException.GatewayTimeout` 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, and what is its timeout?

| Signal | Layer | Default timeout |
| --- | --- | --- |
| `Server: nginx`, log above | nginx | `proxy_connect_timeout` 60s, `proxy_send_timeout` 60s, `proxy_read_timeout` 60s |
| `Server: awselb/2.0` | ALB | Idle timeout 60s |
| `{"message":"Endpoint request timed out"}` | API Gateway | REST APIs: up to 29s before an approved increase; HTTP APIs: 30s maximum |
| `X-Cache: Error from cloudfront`, "CloudFront attempted to establish a connection with the origin" | CloudFront | Origin response timeout 30s |
| `Server: cloudflare`, page "Error 524: A timeout occurred" | Cloudflare | 125s default wait for the origin response |
| `Server: cloudflare`, "Error 504 Gateway time-out" | Origin proxy or Cloudflare timed out | Check the response body and origin logs |

The nginx `proxy_read_timeout` is the time between two successive reads from the upstream, not the whole request. A stream must keep the interval between upstream reads below that timeout. ingress-nginx has its own defaults: connect 5 seconds, send/read 60 seconds. [nginx timeouts](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout), [ingress-nginx settings](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-connect-timeout).

Measure where the time goes:

```bash
curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' https://example.com/report
curl -s -o /dev/null -w 'ttfb=%{time_starttransfer} code=%{http_code}\n' http://127.0.0.1:8000/report   # bypass the proxy
```

If the direct request is slow too, fix the app. If only the proxied request fails, fix the proxy path.

## Common causes by stack

- **nginx/PHP-FPM:** `upstream timed out ... while reading response header from upstream` identifies the read phase. Use `proxy_read_timeout` for HTTP backends, `fastcgi_read_timeout` for PHP-FPM and `uwsgi_read_timeout` for uWSGI; fix the slow operation shown in application logs first. [FastCGI timeout](https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_read_timeout), [uWSGI timeout](https://nginx.org/en/docs/http/ngx_http_uwsgi_module.html#uwsgi_read_timeout).
- **Express, Django or Rails behind a proxy:** the gateway's 504 can arrive while the handler still runs. Correlate the proxy request with app/database timings; apply explicit deadlines to outbound calls and move exports to background jobs. Read the emitting proxy's log rather than looking for a framework-specific 504 exception.
- **AWS ALB:** a connection timeout, a response idle timeout or a TLS-handshake timeout can produce 504. Check target security groups and network ACL return traffic as well as app latency. [ALB 504 causes](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html#http-504-issues).
- **API Gateway:** check the integration's `timeoutInMillis` and API type, then compare the execution log with Lambda duration. Increasing Lambda's own timeout alone leaves the gateway limit unchanged. [REST integration quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html), [HTTP API quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-quotas.html).
- **Kubernetes ingress-nginx:** set the read/send annotations on the Ingress that routes this endpoint, then inspect controller logs for the upstream address and timeout phase. [Ingress annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#custom-timeouts).

## Fix it

1. Find the slow part: slow SQL, an external API call without a timeout, a lock, an exhausted connection pool, or a worker pool with no free workers (all gunicorn or PHP-FPM workers busy). Add timeouts to every outbound call.
2. For work that routinely exceeds the proxy limit, use a job endpoint. Return `202 Accepted` with a status URL and process in the background.
3. Raise timeouts at every hop, from the outermost in. The smallest value wins.

```nginx
location /report {
    proxy_pass http://app;
    proxy_connect_timeout 5s;      # failing fast on a dead host is good
    proxy_read_timeout 300s;       # raised for this endpoint only
    proxy_send_timeout 300s;
}

# PHP-FPM
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_read_timeout 300s;
}
```

```yaml
# ingress-nginx
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
```

Check worker deadlines too: Gunicorn's `--timeout` defaults to 30 seconds of worker silence; for non-sync workers it is not a per-request deadline. A killed worker can leave nginx reporting a prematurely closed connection and 502. Inspect `WORKER TIMEOUT`, PHP `max_execution_time`/FPM `request_terminate_timeout`, or uWSGI `harakiri`. [Gunicorn timeout](https://gunicorn.org/reference/settings/#timeout), [FPM configuration](https://www.php.net/manual/en/install.fpm.configuration.php).

### AWS, Cloudflare and others

- ALB: raise the load balancer attribute `idle_timeout.timeout_seconds`. Keep backend keep-alive timeouts longer than it (see [502](https://howhttpworks.com/status-codes/502)).
- API Gateway: Regional/private REST APIs can request an integration limit above 29 seconds, potentially reducing the account throttle quota. Edge-optimized REST APIs remain capped at 29 seconds. HTTP APIs have a separate, non-increasable 30-second maximum. Apply the approved value to the integration and redeploy.
- CloudFront: raise the origin response timeout in the origin settings (30s default; values above the default quota need a quota increase).
- Cloudflare: its default 125-second origin read timeout produces [524](https://howhttpworks.com/status-codes/524); Enterprise can raise it to 6,000 seconds. A Cloudflare 504 needs its own origin-versus-edge diagnosis. Use polling for jobs that exceed the available wait.

## Client-side handling

Retry a 504 only if the request is safe to repeat. A `GET` is fine with backoff; a `POST` may have been processed even though the response never arrived, so use an idempotency key or check state before retrying.

## Related codes

- [502 Bad Gateway](https://howhttpworks.com/status-codes/502): connection refused, reset or invalid response from the upstream.
- [503 Service Unavailable](https://howhttpworks.com/status-codes/503): no healthy upstream, or deliberate unavailability.
- [524 A Timeout Occurred](https://howhttpworks.com/status-codes/524): Cloudflare connected, then hit an origin read or write timeout.
- [408 Request Timeout](https://howhttpworks.com/status-codes/408): the client was too slow, the reverse of this.
- [499 Client Closed Request](https://howhttpworks.com/status-codes/499): nginx logs this when the client gave up before the upstream answered.

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.
