
A 504 Gateway Timeout means Nginx successfully reached your upstream — PHP-FPM, Apache, or a proxied Node/Python process — but gave up waiting for a response before the upstream finished the request. This is not a connectivity failure. The connection was made. The process was running. It just took longer than the timeout Nginx was configured to tolerate.
That distinction matters because it points you toward the fix. A 504 is either a timeout that's genuinely too short for legitimate work (large uploads, report generation, bulk imports) or a symptom of something upstream running far slower than it should. Raising the timeout blindly fixes the first case and masks the second — the request eventually succeeds, but the underlying slowness is still there, quietly getting worse as traffic grows.
A 502 Bad Gateway means Nginx couldn't get a valid response from the upstream at all — the PHP-FPM socket wasn't there, the process crashed mid-request, or it returned malformed output. A 504 means the upstream was reachable and working, it simply didn't respond in time. One is a connection or process problem; the other is a speed problem.
You can usually tell them apart from the Nginx error log alone. A 502 typically logs something like connect() failed (111: Connection refused) while connecting to upstream or recv() failed (104: Connection reset by peer). A 504 logs upstream timed out (110: Connection timed out) while reading response header from upstream. That single log line tells you whether you're chasing a crash or a bottleneck — don't skip checking it before you touch any config.
One of the most common causes of 504s under real traffic isn't a single slow script — it's every PHP-FPM worker being busy at once, so new requests sit in the kernel's listen backlog waiting for a free worker. If that wait plus the actual processing time exceeds Nginx's timeout, the client sees a 504, even though each individual request would have finished fine on its own.
Check pool saturation with the FPM status page or by watching active vs. total processes:
A non-zero "max children reached" counter means requests are queuing because pm.max_children in your site's pool config was set too low for the traffic and available RAM. Each site's pool file lives at /etc/php83cs/fpm-pools.d/<sitename>.conf, with the pool listening on /run/<sitename>.sock. Raising pm.max_children (within what your server's memory can support) reduces queuing-driven 504s far more reliably than stretching Nginx's timeout.
The other common cause is a single request that's genuinely slow to compute, not queued. A WooCommerce checkout hitting an unoptimized cart-totals query, a report page running an uncached aggregate across a large orders table, a plugin making a synchronous call to a third-party API that's slow to respond, or a script bumping right up against PHP's max_execution_time — all of these can push a single request past Nginx's default read timeout even when the server has plenty of spare capacity.
These show up differently than pool exhaustion: the FPM status page looks healthy, only a handful of processes are active, but specific URLs consistently time out — usually checkout, import, export, or report-generation endpoints. That pattern points at the code path itself, not server capacity.
For requests that are legitimately slow by design — large file uploads, bulk CSV imports, PDF or report generation — the fix is to raise the specific timeouts involved, not just one directive. Nginx's default proxy_read_timeout, proxy_connect_timeout, and fastcgi_read_timeout are all 60 seconds, which is too short for any operation that takes longer than a minute to compute or transfer.
Where you place this matters. CloudStick generates the base vhost at /etc/nginx-cs/vhosts.d/<site>.conf, but that file gets regenerated whenever the site is rebuilt or CloudStick's Nginx package is upgraded — any manual edit there is lost. Custom directives belong in /etc/nginx-cs/extra.d/<site>.d/main-location-pre.conf instead, which is included automatically into the site's location block and survives upgrades and rebuilds untouched. Also bump PHP's own max_execution_time in the site's PHP-FPM pool config to match — raising Nginx's timeout alone won't help if PHP itself kills the script first.
Only raise the timeout for the specific location that needs it — a WordPress admin-ajax import endpoint or a dedicated upload route — rather than globally for the whole site. A storefront checkout that hangs for 5 minutes because of a stuck timeout is a worse experience than one that fails fast at 60 seconds with a clear error.
Before touching any timeout value, confirm where the time is actually going — otherwise you'll raise a number, watch the 504 disappear, and have no idea that a query is getting slower every week as a table grows. Enable MySQL's slow query log and check what's actually running long:
Think in terms of "where is the time actually spent" the way an APM tool like New Relic would break it down — database time, external HTTP calls, PHP execution, or queue wait — rather than treating the whole request as one opaque black box. If the slow query log stays quiet but the request is still slow, suspect an external API call (payment gateway, shipping rate lookup, third-party plugin license check) with no timeout of its own, which can hang for far longer than any Nginx setting.
CloudStick's Web Application Logs section surfaces the per-site Nginx error log directly in the dashboard, so you can see the exact "upstream timed out" line, the URL that triggered it, and the timestamp without SSHing in and tailing a log file by hand — useful for correlating a spike in 504s with a specific import job or traffic surge.
Work the problem in this order: check the Nginx error log to confirm it's really a 504 and not a mislabeled 502, check the PHP-FPM status page for a queuing pattern, check the slow query log for a single expensive request, and only then raise the specific timeout that matches the legitimate slow operation you found — in extra.d/<site>.d/main-location-pre.conf, never in vhosts.d.
Reload Nginx after any config change with sudo systemctl reload nginx-cs, then retest the exact endpoint that was timing out. If it still fails at the new limit, the bottleneck is worse than a config tweak can fix and it's time to look at query optimization, caching, or offloading the slow work to a background job instead of the request/response cycle.

