Troubleshooting

nginx 504 Gateway Timeout — find the slow upstream and fix it

A 504 means nginx waited for PHP-FPM, Node or another upstream and gave up. Read the log, raise the right timeout and fix the slow request behind it.

Published ~10 min read

A 504 Gateway Time-out from nginx means nginx passed the request to your backend (PHP-FPM, a Node app, gunicorn or another server), the backend accepted it, and no response arrived before the read timeout ran out. That timeout is 60 seconds by default. Find the slow request in the error log, fix what makes it slow, and raise proxy_read_timeout or fastcgi_read_timeout only for routes that legitimately run long.

Debian · Ubuntu · find the cause
sudo tail -n 50 /var/log/nginx/error.log

# "upstream timed out (110: Connection timed out) while reading response header from upstream"
#   -> the backend is too slow: see "Why is the upstream slow?" below
# "upstream timed out (110: Connection timed out) while connecting to upstream"
#   -> the backend is unreachable: firewall, wrong IP or port
nginx · a longer timeout for one route only
location /reports/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_read_timeout 300s;
}
Debian · Ubuntu · apply
sudo nginx -t && sudo systemctl reload nginx

What is the difference between 502 and 504?

  • 502 Bad Gateway: nginx got no usable answer. The connection was refused or reset, the socket does not exist, or the backend closed the connection before it sent a response. See nginx 502 Bad Gateway.
  • 504 Gateway Timeout: the connection worked, but the answer did not arrive in time. nginx gave up waiting.

A timeout can still end as a 502. When the backend enforces its own limit first, for example gunicorn’s worker timeout or PHP-FPM’s request_terminate_timeout, it kills the request and closes the connection. nginx then logs upstream prematurely closed … or Connection reset by peer and answers 502. So if you see both codes for the same slow page, it is the same problem, and the backend cut the request off before nginx did.

Which timeout applies?

Each kind of upstream has its own set of three timeouts, and all of them default to 60 seconds:

  • proxy_pass (Node, Python behind gunicorn or uvicorn, another web server): proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout.
  • fastcgi_pass (PHP-FPM): fastcgi_connect_timeout, fastcgi_send_timeout, fastcgi_read_timeout.
  • uwsgi_pass and grpc_pass have the same trio with their own prefix.

The end of the error-log line tells you which one fired:

  • while connecting to upstream: the connect timeout. The backend did not even accept the connection. nginx notes that this one cannot usually exceed 75 seconds.
  • while sending request to upstream: the send timeout. The backend stopped reading the request body, which is rare.
  • while reading response header from upstream: the read timeout, and by far the most common. The backend is working on the request and has not started answering.
  • while reading upstream: the response started, then stalled in the middle. nginx has already sent the status line by then, so the visitor gets a cut-off page instead of a 504.

With several servers in one upstream block, a timeout also makes nginx try the next server, because proxy_next_upstream (and fastcgi_next_upstream) defaults to error timeout. A visitor can then wait a multiple of the timeout before the 504, and the log shows one time per try, separated by commas. Cap it with proxy_next_upstream_tries or proxy_next_upstream_timeout. POST requests are not passed on once they have reached a server.

How do I fix a 504 with PHP-FPM?

A PHP request passes three limits, set in three different files, and they must agree:

  • nginx fastcgi_read_timeout: how long nginx waits for FPM. Default 60 s.
  • PHP max_execution_time in php.ini: default 30 s for web requests. On Linux it counts only the time the script itself runs. Time spent waiting on a database query, a stream or an external HTTP call is not included, so a script that mostly waits can run far longer than this value.
  • FPM request_terminate_timeout in the pool config: wall-clock time after which FPM kills the worker. Default 0, which means off.

Order them so the innermost limit fires first: max_execution_time below request_terminate_timeout, and both below fastcgi_read_timeout. Then PHP stops a runaway script with a clear error in its own log, FPM kills anything stuck in I/O, and nginx never waits longer than the two of them. Applications can override max_execution_time with set_time_limit(), and WordPress plugins often do.

/etc/php/8.3/fpm/php.ini
max_execution_time = 60
/etc/php/8.3/fpm/pool.d/www.conf
request_terminate_timeout = 90s
Debian · Ubuntu
sudo php-fpm8.3 -t && sudo systemctl restart php8.3-fpm

php -v shows the version in those paths, here 8.3. On Rocky and AlmaLinux the same two lines go into /etc/php.ini and /etc/php-fpm.d/www.conf:

Rocky · AlmaLinux
sudo php-fpm -t && sudo systemctl restart php-fpm
nginx · Debian · Ubuntu
location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_read_timeout 120s;
}

Debian 12 ships PHP 8.2, Debian 13 ships 8.4, Ubuntu 22.04 ships 8.1, Ubuntu 24.04 ships 8.3 and Ubuntu 26.04 ships 8.5. Use the paths and the service name that match your version.

How do I fix a 504 with Node.js, gunicorn or uWSGI?

  • Node.js: Node’s HTTP server does not time out a slow handler by default, so nginx’s proxy_read_timeout is usually the first limit a slow request reaches. Raise it per location, as in the quick fix.
  • gunicorn: its worker --timeout defaults to 30 seconds and kills the worker, which nginx reports as a 502. Raise the gunicorn timeout and proxy_read_timeout together, with nginx’s a little higher.
  • uWSGI: its harakiri option plays the same role. Pair it with uwsgi_read_timeout.
  • Another nginx or Apache behind this one: every proxy in the chain has its own timeout. The shortest one wins.

Why is the upstream slow?

Raising a timeout only hides the problem until traffic grows. First find out which requests are slow. Add request timing to the access log:

nginx · http block
log_format timed '$remote_addr "$request" $status '
                 'rt=$request_time uct=$upstream_connect_time '
                 'uht=$upstream_header_time urt=$upstream_response_time';
access_log /var/log/nginx/timed.log timed;

uct is the time to connect to the backend, uht the time until its response headers arrived, which is where most 504s happen, and urt the whole backend response; a - means that step never completed. The new file sits next to the normal access.log, which keeps its usual format. A server block with its own access_log line inherits neither, so add the timed line there as well. Then call the backend directly, bypassing nginx, to confirm:

on the server · time the backend itself
curl -o /dev/null -s -w 'total %{time_total}s\n' http://127.0.0.1:3000/slow-page

Slow database queries

One of the most common causes. A missing index or a query that grows with the table turns a fast page into a slow one over months. See what is running right now:

MariaDB · MySQL · in sudo mysql
SHOW FULL PROCESSLIST;
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;        -- slower than 1 s; applies to new connections
SHOW VARIABLES LIKE 'slow_query_log_file';
PostgreSQL · in sudo -u postgres psql
SELECT pid, now() - query_start AS runtime, query
FROM pg_stat_activity WHERE state = 'active' ORDER BY runtime DESC;

PHP-FPM is out of workers

If every worker is busy, new requests wait in a queue, and the wait counts against nginx’s timeout. FPM says so in its log (/var/log/php8.3-fpm.log on Debian and Ubuntu, /var/log/php-fpm/error.log on Rocky):

FPM log
WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

The pool config Debian and Ubuntu ship sets pm.max_children = 5, which is low for a real site. Rocky and Alma ship 50. Check what yours says with grep -E '^pm' /etc/php/8.3/fpm/pool.d/www.conf (or /etc/php-fpm.d/www.conf). Size pm.max_children by memory: the RAM you can give PHP, divided by the average size of one worker.

Debian · Ubuntu · average FPM worker size
ps -o rss= -C php-fpm8.3 | awk '{s+=$1; n++} END {print s/n/1024 " MB"}'
# Rocky: -C php-fpm

To find the slow scripts themselves, turn on FPM’s slow log in the pool config: request_slowlog_timeout = 5s and slowlog = /var/log/php8.3-fpm-slow.log (on Rocky and Alma the stock pool already points slowlog at /var/log/php-fpm/www-slow.log). Every request slower than five seconds then leaves a PHP stack trace showing where it was waiting.

CPU, steal time or memory pressure

If everything is slow, not just some pages, look at the machine. High CPU, or high st (steal) in top, is covered in VPS high CPU usage. Heavy swapping (non-zero si and so columns in vmstat 1) slows every request down, and workers that disappear completely point to the OOM killer. See process killed by the OOM killer.

Slow external calls

A request that calls a payment API, a mail server or another service waits as long as that service takes, and HTTP client libraries often have no timeout by default. Set an explicit timeout in your code (curl’s CURLOPT_TIMEOUT, the timeout= argument in Python’s requests, an AbortSignal in Node) so one slow dependency fails fast instead of holding a worker for a minute.

What if a request legitimately takes minutes?

Exports, report generation, imports, video processing and bulk e-mail do not belong in a request that someone waits for. Answer at once with 202 Accepted and a job ID, process the job in a background worker, and let the client poll for the result or get a notification when it is ready. Laravel queues, Celery, RQ, BullMQ or a small systemd service that reads jobs from a table all do this. Raising the timeout to ten minutes only moves the failure to the next layer, and keeps a worker busy for the whole time.

If the slow page is the same for every visitor, such as a report, a feed or a dashboard, cache it: with nginx’s proxy_cache or fastcgi_cache, or the application’s own cache, only the first request after the cache expires waits for the backend.

What does “while connecting to upstream” mean?

while connecting to upstream means nothing answered the TCP handshake. A backend that is down on the same machine usually gives a fast Connection refused, which is a 502. A timeout instead points to:

  • A firewall that drops packets between nginx and a backend on another host. Drop rules make connections time out, reject rules make them fail at once.
  • A wrong or stale IP, such as an old address in proxy_pass or a backend host that moved.
  • Docker networking: a container IP that changed after a restart. Proxy to a published port on 127.0.0.1, or put nginx on the same Docker network and use the service name.
on the nginx server · test the path to the backend
curl -v --connect-timeout 5 http://10.0.0.5:8080/

Put in the address and port from your proxy_pass or upstream block. A timeout here (curl: (28) Failed to connect … Timeout was reached), from the same machine nginx runs on, confirms the packets are dropped on the way.

Is it a Cloudflare 524 or an nginx 504?

Behind Cloudflare there are two clocks. Cloudflare waits for your origin for 125 seconds by default. Older guides say 100; Cloudflare’s documentation now gives 125. If the origin has not answered by then, visitors get Cloudflare’s own 524 “A timeout occurred”, whatever nginx is configured to allow. Only Enterprise plans can raise that limit, up to 6,000 seconds.

  • A 504 behind Cloudflare means nginx (or something behind it) gave up first. Everything above applies.
  • A 524 means your origin was still working when Cloudflare stopped waiting. Raising proxy_read_timeout past 125 seconds changes nothing for proxied traffic; the fixes for that case are in Cloudflare error 524.

For a genuinely long request, move the job to a queue as above, or serve that endpoint from a separate hostname set to DNS only (grey cloud), which Cloudflare does not proxy. If you are setting that record up, see pointing a domain at your VPS.

Raised the timeout and still get 504?

  • The change is not active. nginx -t failed, nginx was not reloaded, or the request is served by a different server or location block. sudo nginx -T | grep -n timeout shows every timeout in the loaded config.
  • Another proxy in the chain still uses its own default: a load balancer (AWS’s Application Load Balancer, for example, has a 60-second idle timeout by default), a second nginx in front, or an ingress controller.
  • The backend never answers. A deadlock or a request waiting on a lock will time out at any limit. Look at the slow log and the database’s process list while it is happening.

If nginx answers 403 Forbidden instead, that is a permissions problem, covered in nginx 403 Forbidden. And when the slow part turns out to be PHP or Node simply needing more CPU, a Hi-CPU KVM plan gives PHP-FPM and Node the headroom to answer well inside the timeout.

Frequently asked

What causes a 504 Gateway Timeout in nginx?

nginx passed the request to a backend such as PHP-FPM, Node or gunicorn and received no response within the read timeout, 60 seconds by default. The usual reasons are slow database queries, a backend with no free workers, CPU or memory pressure, or a slow external API call the request is waiting on.

How do I increase the timeout in nginx?

Set proxy_read_timeout for proxy_pass upstreams or fastcgi_read_timeout for PHP-FPM, ideally only in the location that needs it, for example 'proxy_read_timeout 300s;', then run 'sudo nginx -t && sudo systemctl reload nginx'. For PHP, raise max_execution_time and request_terminate_timeout to what the request needs as well, keeping both a little below the nginx value so PHP ends a runaway script before nginx gives up.

502 or 504: what is the difference?

502 Bad Gateway means nginx got no usable response: the connection was refused or reset, or the backend closed it early. 504 Gateway Timeout means the connection worked but the response did not arrive in time. The line in /var/log/nginx/error.log says which one you have.

Why do I still get 504 after raising proxy_read_timeout?

Either the change is not active or another layer has a shorter limit. Check with 'sudo nginx -T' that the edited location serves the request and that nginx was reloaded, then look for another proxy in the chain, such as a load balancer with its own idle timeout or a second nginx in front.

Related articles

All troubleshooting