How HTTP Works

Debug guide · you're seeing

  • 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

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.

Reviewed 6 min readintermediate6 sourcesTry itMarkdown
On this page

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/1.1 504 Gateway Time-out
Server: nginx/1.27.0
Content-Type: text/html
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 saysDirectiveWhat it measures
while connecting to upstreamproxy_connect_timeoutTime 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 upstreamproxy_send_timeoutMaximum gap between two writes of the request to the upstream. Matters for large uploads to a slow upstream.
while reading response header from upstreamproxy_read_timeoutMaximum 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

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

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_read_timeout 180s;     # default 60s
}
; 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:

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:

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). 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

POST /reports HTTP/1.1

HTTP/1.1 202 Accepted
Location: /reports/8f3a
Retry-After: 5
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:

# 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.

Frequently asked questions

What is the default proxy_read_timeout in nginx?

It is 60 seconds, and so are proxy_connect_timeout and proxy_send_timeout. proxy_read_timeout is not a total request budget: it is the longest nginx will wait between two successive reads from the upstream, so a response that trickles data every few seconds can run far longer than 60 seconds.

Which timeout do I change for PHP-FPM?

With fastcgi_pass use fastcgi_read_timeout (default 60s), not proxy_read_timeout. Also raise PHP-FPM request_terminate_timeout and PHP max_execution_time, otherwise the worker is killed at its own limit and you get a 502 instead of a longer wait.

Why does increasing proxy_read_timeout not stop my 504?

Something in front of nginx has a shorter limit. An AWS Application Load Balancer defaults to a 60 second idle timeout, CloudFront to a 30 second origin response timeout, and Cloudflare gives up after 125 seconds unless an Enterprise zone raised it. The 504 you see may not be generated by nginx at all; check the Server header of the failing response.

What is the difference between a 504 and Cloudflare 524?

A 504 means the gateway gave up waiting for the upstream. Cloudflare 524 means Cloudflare connected to your origin but the origin did not return an HTTP response within the Proxy Read Timeout, 125 seconds by default (up to 6,000 on Enterprise). Cloudflare returns 522 when it cannot even complete the TCP connection.

Is raising the timeout the right fix?

Only for work that is legitimately slow but bounded. If a request regularly needs minutes, return 202 Accepted with a status URL and do the work in a background job, or stream progress. Longer timeouts hold connections and worker slots open, which lets a slow dependency take the whole service down.

Sources

  1. nginx: ngx_http_proxy_module proxy_read_timeoutnginx.org
  2. nginx: ngx_http_fastcgi_module fastcgi_read_timeoutnginx.org
  3. MDN: 504 Gateway Timeoutdeveloper.mozilla.org
  4. RFC 9110 Section 15.6.5: 504 Gateway Timeoutrfc-editor.org
  5. AWS Docs: Idle timeout for your Application Load Balancerdocs.aws.amazon.com
  6. Cloudflare Docs: Error 524developers.cloudflare.com

Keep going

Browse /search