Fixing nginx 504 Gateway Time-out: find the cause instead of raising the timeout

Published on 20 min read

With a 504 the backend was reachable, it only answered too slowly. How to use a timing log to pin down where the time goes, which of the many timeouts really applies, and why a higher timeout usually only postpones the outage.

A 504 Gateway Time-out is the most patient of all error pages. nginx accepted the request, passed it on to the backend and then waited until an internally set timeout ran out. The backend was reachable the whole time, it just did not answer in time. That is precisely what separates it from the adjacent status code: with a 502 Bad Gateway the backend answers wrongly or not at all, with a 504 it answers too slowly. This guide shows you how to measure where the time is really spent, which of the many timeouts actually applies, and why raising that timeout is almost always the worst of the available answers.

Everything here applies to Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS and Ubuntu 22.04 LTS. The commands are written for a root shell, as a normal user put sudo in front. The examples use PHP 8.4, replace the version number with the one your system runs:

SystemPHPServiceConfiguration
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/
ls /etc/php/

The nginx directives carry the same names on all four systems. The differences sit on the PHP side and on the database side, and they are noted where they matter.

The layer that gave up decides where you look

Before you open a single file, answer one question: which layer gave up? The status code already tells you.

CodeWhat happenedWhere to look
500 Internal Server ErrorThe backend answered, and the answer was an errorLog of the application
502 Bad GatewayThe connection never came up, or it broke offService, socket, permissions, crashes
504 Gateway Time-outThe connection was up, the answer did not arrive within the timeoutRuntime in the backend
408 Request TimeoutThe visitor did not finish sending their own request in timeUploads, slow connections
499 (log only)The visitor gave up before nginx was doneToo slow, but still below the timeout

The 499 row is the most underrated one. It is not an error, it is an early warning system: the visitor closed the tab because the page took too long. If a 504 disappears after you raise the timeout and 499s show up in its place, nothing has been solved.

awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

Field 9 holds for the default format combined. Many 504s next to few 499s point at a handful of heavy pages, the opposite picture points at an application that is sluggish throughout.

Before you change anything: the way back

The diagnosis in the next two sections is read-only. It only gets risky where you start touching configurations, and that can stop the site in three ways: a broken nginx configuration keeps the web server from starting, a broken pool file keeps PHP-FPM from starting, and generously raised limits can eat up the RAM. The last case is the nastiest, because the kernel then starts killing processes, and it does not necessarily hit the one that caused the trouble. If it hits the SSH service, the server can no longer be operated over the network.

So make copies first, below /root and never in the web directory. The timestamp in the name matters, because you rarely intervene only once and cp -a overwrites an existing copy without a word:

mkdir -p /root/backups
cp -a /etc/nginx/nginx.conf /root/backups/nginx.conf.$(date +%F-%H%M)
cp -a /etc/nginx/sites-available/example.com /root/backups/example.com.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/php.ini /root/backups/php.ini.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)

The way back is three lines, and the order is deliberate:

cp -a /root/backups/example.com.2026-09-03-1030 /etc/nginx/sites-available/example.com
nginx -t
systemctl reload nginx

Use reload instead of restart for as long as you can. reload only takes the new configuration over when it is free of errors. A restart stops the running process first and leaves you without a web server if something is wrong.

If the server stops answering altogether, the KVM root servers and dedicated servers from KernelHost give you the VNC console in the customer panel. It hangs off the virtualization layer, or off the uplink itself, and not off the network stack of the guest system, so it still works when no service is reachable any more. Log in there once beforehand and make sure you know the root password. Check command after every change:

systemctl is-active nginx php8.4-fpm
free -m

The line in the error log that decides the case

A 504 always leaves a trace:

2026/09/03 10:12:33 [error] 812#812: *5 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 203.0.113.7, server: example.com,
request: "GET /report.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock:"

What matters is not the error number 110, which reads the same for every 504, but the phase behind it:

  • while connecting to upstream: the connection was never established. With a remote backend this is almost always a packet filter that drops packets instead of rejecting them, because a rejection would come back immediately and produce a 502.
  • while sending request to upstream: nginx could not get the request body out, which is typical for large uploads.
  • while reading response header from upstream: the normal case. The backend received everything and is still computing, without sending even the first header line.
  • while reading upstream: the headers arrived, then the body stalled. You see this with streaming responses and exports.
grep -n "upstream timed out" /var/log/nginx/error.log | tail -20

Many virtual hosts write into an error log of their own. Where that file lives, and why the uppercase -R is needed when you search in sites-enabled, is covered in the article on the 502. If the search stays empty although the browser shows a 504, the error does not come from this nginx.

Where the time goes: measure instead of guess

nginx can log per request how long the backend needed. This is the most important step of the whole diagnosis, because it answers the question "application or network" without any guessing. In the http block of /etc/nginx/nginx.conf:

log_format kh_timing '$time_iso8601 $status rt=$request_time '
                     'uct=$upstream_connect_time uht=$upstream_header_time '
                     'urt=$upstream_response_time "$request"';

In the affected server block, add a second log line. The existing access log stays untouched, nginx writes both:

access_log /var/log/nginx/timing.log kh_timing;
nginx -t
systemctl reload nginx
tail -n 5 /var/log/nginx/timing.log

After a few minutes of traffic, pull the slowest requests to the top:

awk '{ t=$3; sub(/^rt=/, "", t); print t, $0 }' /var/log/nginx/timing.log | sort -rn | head -20
ObservationInterpretationNext step
uct high with a local backendThe connection setup is stallingFull listen queue on the socket, slow name resolution
uht and urt almost equal, both highThe backend computes before it sends the first header lineApplication, database, external API
uht small, urt highThe header came fast, the body tricklesStreaming, exports, loops over many records
urt small, rt highThe backend was fast, the time was lost afterwardsThe visitor's connection, very large response
A hyphen instead of a numberNo backend was involved at allStatic file, or an abort before the request was passed on

Two details save a lot of time. Several comma separated values in one field mean the request went to more than one target, so there was a retry. And the sharpest hint of all: if the measured duration matches the configured value to the second, say 60.001 seconds with a timeout of 60, then the timeout fired and the backend did not give up on its own. Odd values such as 43.7 seconds show that something else was the brake.

Which timeout actually applies

nginx has a good dozen directives for timeouts, and the most commonly wasted hour comes from somebody touching the wrong one. Which one applies depends on the module that handles the location block.

DirectiveDefaultApplies in blocks withEffect when it expires
proxy_connect_timeout60sproxy_pass504, "while connecting to upstream"
proxy_send_timeout60sproxy_pass504, "while sending request to upstream"
proxy_read_timeout60sproxy_pass504, the decisive timeout for proxy backends
fastcgi_connect_timeout60sfastcgi_pass504, as above, for PHP-FPM
fastcgi_send_timeout60sfastcgi_pass504, as above
fastcgi_read_timeout60sfastcgi_pass504, the decisive timeout for PHP
send_timeout60severywhereno 504, the connection to the visitor is closed
client_body_timeout60severywhere408, not 504

The timeout applies between two read operations, not to the whole response. A download that takes ten minutes and delivers data without a break runs through. A backend that stays silent for 61 seconds gets thrown out. With stalling exports it therefore often helps more to have the application output something at regular intervals than to raise the timeout.

send_timeout does nothing about a 504. That timeout covers the transfer to the visitor. When it expires there is no error page, only an aborted download. It becomes relevant once you switch buffering off with proxy_buffering off;, because a slow visitor then drags the backend down as well.

nginx also accepts directives that have no effect in the block they sit in. A proxy_read_timeout 300s; inside a PHP block with fastcgi_pass is syntactically flawless, nginx -t reports syntax is ok, and the page still breaks off after 60 seconds. The reverse holds just as well. This is the most common reason why a raised timeout stays without effect. What really applies shows up in the assembled configuration:

nginx -T | grep -E "read_timeout|send_timeout|fastcgi_pass|proxy_pass"

If one single path really has to run longer, set the timeout exactly there and nowhere else. The equals sign turns it into an exact match, and that wins against the general block location ~ \.php$ which handles the remaining PHP files:

location = /admin/export.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_read_timeout 300s;
}
nginx -t
systemctl reload nginx
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/admin/export.php

PHP: why max_execution_time rarely applies here

The obvious assumption is that PHP terminates a hanging script by itself. In exactly this case that is usually wrong. First the measuring trap: php -i queries the command line variant. That one uses a configuration of its own under /etc/php/8.4/cli/ and runs without a runtime limit anyway, so it says nothing about FPM. These two places are the ones that count:

grep -n "^max_execution_time" /etc/php/8.4/fpm/php.ini
grep -rn "max_execution_time" /etc/php/8.4/fpm/pool.d/

In the pool file the value can be overridden with php_value[max_execution_time] or php_admin_value[max_execution_time]. A value set through php_admin_value can no longer be changed from inside the application with ini_set(). If your framework raises the runtime itself and that suddenly has no effect, this is why.

And now the actual point: on Linux, time spent waiting inside system calls does not count. The clock only runs while the script itself is computing. When it waits for a database query, for an external API or for the file system, the clock stands still. A script can therefore stick to a hanging query for ten minutes without the runtime limit ever firing. What terminates it in the end is the nginx timeout, and the result is the 504.

An uncomfortable rule follows from that: max_execution_time protects you against endless loops in your own code, not against waiting. The only hard limit on the PHP side is request_terminate_timeout in the pool file, which clears the worker process away no matter what it is stuck on. It produces a 502 though, not a 504. Order the timeouts so they rise from the inside out, so that the layer which can still produce a readable message fires first. For computing scripts that works, for waiting ones it does not, for the reason just given. There, the only option left is to limit the waiting itself.

Why raising the timeout is usually the wrong answer

Do the math for a moment. A pool with pm.max_children = 10 has ten worker processes. One page needs 90 seconds. Ten simultaneous calls therefore occupy every single one of them for a minute and a half. During that time nobody gets a PHP page delivered any more, not even the home page. A slow subpage has turned into an outage. Three mechanisms make it worse:

  • The visitor reloads. That creates an extra request without releasing the old one. PHP only notices that a visitor is gone the next time the script outputs something, and a computing script outputs nothing for a long while.
  • nginx retries on its own. With an upstream block that has several targets, proxy_next_upstream defaults to error timeout. A request that ran into the timeout goes to the next server, and the expensive query runs a second time. Writing requests are excluded, reading ones are not. Switch it off with proxy_next_upstream error;.
  • Monitoring retries as well. A check interval of 60 seconds against a page that needs 90 seconds creates a permanent load that never gets worked off.

On top of that, nobody waits five minutes for a web page. A timeout of 300 seconds turns a one minute problem into a five minute problem, and the visitor is long gone while the worker process keeps computing.

How full it really is shows on the status page of PHP-FPM. Set pm.status_path = /fpm-status in the pool file and create an entry point in the server block that is reachable locally only:

location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
systemctl reload php8.4-fpm
nginx -t
systemctl reload nginx
curl -s -H "Host: example.com" http://127.0.0.1/fpm-status

Watch active processes, listen queue and max active processes. If listen queue stays above zero permanently, the number of worker processes does not cover the current runtime of the pages. That is the moment to bring the runtime down, not to raise the timeout.

The three usual time sinks

Database

In the majority of cases the time sits here. Look at what is running right now first. On all four systems the root access goes through the Unix socket by default, so the command works without a password:

mysql -e "SHOW FULL PROCESSLIST;"

The interesting columns are Time and State. Values such as Sending data or Waiting for table metadata lock next to double digit seconds are your case. For a systematic search use the slow query log, which can be switched on while the database keeps running:

mysql -e "SET GLOBAL slow_query_log = 1; SET GLOBAL long_query_time = 1;"
mysql -e "SHOW VARIABLES LIKE 'slow_query_log_file';"

Read the file name from the second output, because it differs: Debian usually goes with MariaDB and writes to /var/log/mysql/mariadb-slow.log, while on Ubuntu the file is named differently depending on the installed server. Evaluate it after a few minutes and switch it off again, the log costs write load:

mysqldumpslow -s t /var/log/mysql/mariadb-slow.log | head -30
mysql -e "SET GLOBAL slow_query_log = 0;"

The switch -s t sorts by total time. Check the most expensive query with EXPLAIN, and in most cases an index is missing on exactly the column that gets filtered or sorted on. SET GLOBAL takes effect immediately, but it does not survive a restart of the database. There is an upper limit per query on the database side as well: MariaDB has max_statement_time in seconds, MySQL has max_execution_time in milliseconds, the latter for reading queries only. That way your application gets a clean error instead of an occupied worker process.

External APIs

If your site queries a payment provider or a license server on every call, their outage becomes your 504. Measure that call separately, from the server:

curl -o /dev/null -s -w "dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://api.example.com/status

If dns already stands out, the cause is name resolution and not the remote side, cross-check with time getent hosts api.example.com. In the code, every external call needs a tight timeout of its own: with cURL those are CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT. Anyone who reaches for file_get_contents() on a URL instead ends up with default_socket_timeout from the php.ini, and that one defaults to 60 seconds:

grep -n "default_socket_timeout" /etc/php/8.4/fpm/php.ini

The rule of thumb: the sum of all external timeouts within one request has to stay below fastcgi_read_timeout. Otherwise nginx cuts things off before your code can output a readable error page.

File system and RAM

A full disk makes writes slow or impossible, and that hits session files, caches and logs at the same time. Check both, the space and the inodes:

df -h
df -i

The second command is the one people forget. A partition can sit at 40% usage and still refuse to take another file once the inodes are used up. The second candidate is a shortage of memory: as soon as the system starts swapping, every request turns sluggish without any particular query being at fault.

vmstat 1 5

If the columns si and so stay above zero permanently, the kernel is swapping out and in without a break, and you have a memory problem, not a timing problem. The clean way to deal with that is described in setting up swap and avoiding out-of-memory. The third candidate is network drives: a hanging NFS mount point blocks every process that touches it, and that includes your diagnosis command. So put a time limit in front of it:

timeout 5 df -h

Long tasks do not belong in the request

Some tasks simply take their time: an annual report, an import with 200,000 rows, an image conversion. The mistake is not that they run long, it is that a web server waits for them. The clean shape has three parts:

  1. The request creates a job, in a table or in a queue, and answers immediately. The fitting status code is 202, together with a URL where the state can be polled.
  2. A worker process outside nginx picks the jobs up and gets them done. It has no timeout breathing down its neck, because nobody is waiting for it.
  3. The interface polls the state. That query is always fast, no matter how long the task takes.

Run the worker process as a service of its own, so that it comes back by itself after a crash and after a reboot. What such a unit looks like is shown in creating a systemd service. For tasks at fixed intervals a cron job is enough, and there you need a lock so that two runs do not overtake each other. With -n the second run aborts immediately instead of waiting:

flock -n /run/lock/kh-worker.lock /usr/bin/php /var/www/html/worker.php

The common frameworks ship the queue ready made, it only has to be run: with Laravel that is php artisan queue:work, with Symfony php bin/console messenger:consume plus the name of your transport. Both belong in a systemd unit, not in a terminal window.

One special case deserves an explicit mention: WordPress starts scheduled tasks inside visitor requests by default. A visitor therefore pays with waiting time so that an update check can run in the background. The line define('DISABLE_WP_CRON', true); in wp-config.php, above the reference to wp-settings.php, switches that off. After that you call the due tasks yourself at regular intervals:

wp cron event run --due-now --path=/var/www/html

Whatever has to stay synchronous gets a location block of its own with a timeout of its own, plus a cap so that this one path cannot occupy every worker process: limit_conn_zone $binary_remote_addr zone=export:10m; in the http block and limit_conn export 1; in the block concerned. Further attempts then get a 503 instead of an occupied server.

Common errors and solutions

Message, word for wordMeaning and remedy
upstream timed out (110: Connection timed out) while reading response header from upstreamThe normal case. The backend takes too long to compute. Evaluate the timing log and identify the time sink before you touch the timeout.
upstream timed out (110: Connection timed out) while connecting to upstreamThe connection setup ran into the timeout. With a remote backend this is almost always a packet filter that drops instead of rejecting, with local PHP-FPM it is a full listen queue on the socket.
upstream timed out (110: Connection timed out) while reading upstreamThe headers arrived, then the body stalled for longer than the timeout. Typical for exports that compute for a long stretch in between.
nginx: [emerg] "fastcgi_read_timeout" directive is not allowed here in /etc/nginx/nginx.conf:12The directive sits outside of http, server or location, usually by accident right at the top of the file.
nginx: [emerg] unknown directive "proxy_read_timout" in /etc/nginx/sites-enabled/example.com:31A typo. nginx checks names, not intentions. The line number is in the message.
PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/export.php on line 42Here the PHP runtime limit did fire for once, so the script was computing and not waiting. Produces a 500 or a blank page, not a 504.
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transactionAnother transaction holds the row, and the limit is 50 seconds by default. The cause is almost always a transaction that stays open for too long.
cURL error 28: Operation timed out after 60000 millisecondsAn external API is not answering. Set a shorter timeout of your own in the code so that your application keeps control.
504 in the browser, but the search for upstream timed out stays emptyThe 504 does not come from this nginx, it comes from a service in front of it such as a load balancer or a second proxy. Some of them report a status code of their own for it.
Still breaking off after exactly 60 seconds although the timeout is set to 300The changed directive belongs to the wrong module, or another block wins. nginx -T shows what really applies.

How you can tell that it is fixed

A command without an error message proves nothing, and a single successful call proves nothing either. Four pieces of evidence that hold up together:

  1. Status code and duration directly on the server, so that no cache prettifies the result. What you expect is a 200 and a duration clearly below the timeout. A 200 after 58 seconds with a timeout of 60 is not a success, it is the next outage as soon as the load rises a little. What counts here is the worst of twenty calls:
    for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/report.php; done | sort -k2 -n | tail -3
  2. The error log stays quiet. Truncate it before the test with truncate -s 0 /var/log/nginx/error.log, trigger the calls, then look inside again. An empty file is the actual proof.
  3. The queue of PHP-FPM sits at zero. As long as something is waiting under listen queue on the status page, the cause has only been moved.
  4. A reboot changes nothing. The point that gets skipped most often. Values from SET GLOBAL, processes started by hand and directories you created yourself below /run do not survive it. Check systemctl is-enabled nginx php8.4-fpm and reboot the server once in a controlled way, while you are still watching.

With the KVM root servers and dedicated servers from KernelHost you handle that reboot, console access included, in the customer panel, even while the web service is delivering nothing at all. The servers sit in the maincubes datacenter in Frankfurt am Main (TÜV TIER3+ certified), with filtering in the network in front of them.

Quick checklist for an emergency

  1. Count the status codes in the access log. 504, 499 and 502 side by side say more than any single one of them.
  2. grep "upstream timed out" /var/log/nginx/error.log, note the phase in the message.
  3. Switch the timing log on, compare uct, uht and urt. If the duration matches the configured timeout to the second, the timeout fired.
  4. Identify the time sink: database, external API, file system or memory.
  5. Use nginx -T to check which timeout applies in that block before you change one.
  6. Only then decide: fix it, move it into a queue or, as a last resort, raise the timeout for exactly this one path.
  7. After the fix: truncate the log, measure twenty calls, reboot the server once in a controlled way.

Frequently asked questions

What is the difference between 502 Bad Gateway and 504 Gateway Time-out?
With a 502 the connection to the backend never comes up, or it breaks off, so the backend answers wrongly or not at all. With a 504 the connection was there, the answer just did not arrive within the timeout: the backend was reachable the whole time and answered too slowly. A 500 on the other hand means that the backend did answer and that the answer was an error. That gives you the direction to search in: with a 502 you check the service, the socket and the permissions, with a 504 you check the runtime in the backend.
I set fastcgi_read_timeout to 300 seconds and the page still breaks off after 60 seconds. What causes that?
Almost always the fact that the changed directive belongs to the wrong module, or that another block wins. nginx also accepts directives that have no effect in the block they sit in: a proxy_read_timeout 300s; inside a PHP block with fastcgi_pass is syntactically flawless, nginx -t reports "syntax is ok", and the page still breaks off after 60 seconds. The reverse holds just as well. What really applies shows up in the assembled configuration through nginx -T plus a search for read_timeout, send_timeout, fastcgi_pass and proxy_pass. If one single path has to run longer, set the timeout in a location block of its own with an equals sign, because an exact match wins against the general PHP block.
Does a higher timeout solve the problem?
In most cases it only postpones it. A pool with pm.max_children = 10 has ten worker processes. If a page needs 90 seconds, ten simultaneous calls occupy every single one of them for a minute and a half, and during that time nobody gets a PHP page delivered any more, not even the home page. A slow subpage has turned into an outage. On top of that, nobody waits five minutes for a web page. If the 504 disappears after you raise the timeout and 499s show up in the access log instead, the visitor gave up on their own and nothing has been solved.
Which line in the error log belongs to a 504?
A 504 always leaves a line of the form "upstream timed out (110: Connection timed out) while reading response header from upstream", which you find with grep -n "upstream timed out" /var/log/nginx/error.log. What matters is not the error number 110, which reads the same for every 504, but the phase behind it: "while connecting to upstream" stands for a connection that was never established, "while sending request to upstream" for nginx not getting the request body out, "while reading response header from upstream" for the normal case in which the backend computes without sending even the first header line, and "while reading upstream" for a body that stalled after the headers had arrived.
Why does max_execution_time not terminate my hanging PHP script?
Because on Linux, time spent waiting inside system calls does not count. The clock only runs while the script itself is computing. When it waits for a database query, for an external API or for the file system, the clock stands still, and the script can stick to a hanging query for ten minutes without the runtime limit ever firing. What terminates it in the end is the nginx timeout, and the result is the 504. Watch out for the measuring trap as well: php -i queries the command line variant, which uses a configuration of its own and runs without a runtime limit anyway. What counts is the php.ini of FPM and the pool file. The only hard limit on the PHP side is request_terminate_timeout, and that one produces a 502, not a 504.
How do I tell whether the timeout fired or the backend gave up on its own?
By the measured duration. Use a log_format of your own to write $request_time, $upstream_connect_time, $upstream_header_time and $upstream_response_time into an additional file. If the duration matches the configured value to the second, say 60.001 seconds with a timeout of 60, then the timeout fired and the backend did not give up on its own. Odd values such as 43.7 seconds show that something else was the brake. Several comma separated values in one field mean a retry against a second target, and a hyphen instead of a number means that no backend was involved at all.
The browser shows a 504 but the search for "upstream timed out" stays empty. Where does the error come from?
Then the 504 does not come from this nginx, it comes from a service in front of it such as a load balancer or a second proxy, and some of them report a status code of their own for it. Check one thing before you go there though: many virtual hosts write into an error log of their own, so you may simply be searching in the wrong file.
What do I do with tasks that naturally take longer than any sensible timeout?
Take them out of the request. The request creates a job and answers immediately, fittingly with status code 202 and a URL where the state can be polled. A worker process outside nginx does the job, with no timeout breathing down its neck, and the interface only polls the state. Run that worker process as a service of its own so that it comes back by itself after a crash and after a reboot, and protect recurring runs with flock -n so that two runs do not overtake each other. Whatever has to stay synchronous gets a location block of its own with a timeout of its own, plus a cap through limit_conn so that this one path cannot occupy every worker process.

nginx PHP-FPM 504 Gateway Time-out Timeout Debian Ubuntu Troubleshooting Linux administration