504 Gateway Time-out, so they raise max_execution_time in PHP to five minutes, restart PHP, and try again. The same 504 shows up at almost exactly the same moment as before. PHP was rarely the part giving up. Nginx was, because it has its own timeout for waiting on PHP, and nobody touched it.
This write-up uses a post-mortem shape, but it describes a common pattern, not one specific incident. The timeline is the order of investigation that usually plays out at a support desk.
What users saw when the import failed
504 Gateway Time-out page. Short pages on the same site load fine. Nothing looks broken except the one slow task.
The side effect causes more damage than the error page. When Nginx gives up, PHP often keeps working in the background, because PHP usually only notices the connection is gone when it tries to send output. So the import may partly or fully finish after the user has already seen a failure. The user then clicks the button again, and you end up with duplicate orders, double-imported products, or two copies of a report job fighting over the same rows. Before you retry a timed-out job, check whether the first attempt actually stopped.
Where the investigation went first
php.ini, in a .user.ini, through a control panel's PHP settings, or with set_time_limit() in the code.
The change often fails for two separate reasons.
The first is that the change may not have landed where PHP-FPM reads it. PHP-FPM (the FastCGI Process Manager, the service that runs PHP for Nginx) uses its own php.ini and pool files. Those differ from the CLI's. Running php -i in a terminal shows the CLI configuration, not what the website gets. A pool file can also force the value with php_admin_value, which scripts and .user.ini files cannot override. The reliable check is a temporary phpinfo() page served through the website, removed right after.
The second reason matters more. Even with the PHP value correctly applied, PHP was never the layer timing out. On Linux, max_execution_time counts the script's own execution time. It does not count time spent waiting on database queries, stream operations, or external calls. A report that spends most of its life waiting on MySQL can run far past its PHP limit in wall-clock terms without PHP stopping it. Nginx, on the other hand, counts real seconds.
The log line that explained it
BASH
# Find upstream timeouts in the Nginx error log (path varies by distro and panel)
sudo grep -n 'upstream timed out' /var/log/nginx/error.log | tail -n 5
# Show the timeout values Nginx actually loaded, including included files
sudo nginx -T 2>/dev/null | grep -n -E 'fastcgi_(read|send|connect)_timeout'upstream timed out (110: Connection timed out) while reading response header from upstream, followed by the client, server, request, and the FastCGI upstream address. "Upstream" means PHP-FPM here, and "while reading response header" means Nginx was still waiting for PHP to send anything at all.
If the second command prints nothing, Nginx is running on its defaults. fastcgi_read_timeout defaults to 60 seconds. One detail about it gets misread a lot: the timeout applies between two successive reads, not to the whole response. A script that flushes some output every few seconds keeps resetting the clock. A script that stays silent until it's done, which is how most imports and reports behave, gets cut off at 60 seconds of silence.
The status code also tells you which layer acted:
| Layer | Setting | What it measures | What the user typically sees |
|---|---|---|---|
| PHP | max_execution_time | Script execution time (not DB or I/O wait on Linux) | A PHP fatal error, or a 500 if errors are hidden |
| PHP-FPM pool | request_terminate_timeout | Wall-clock time per request; disabled by default | 502, since the worker is killed mid-request |
| Nginx | fastcgi_read_timeout | Silence between reads from PHP-FPM | 504 |
| CDN or load balancer | Provider-specific | Its own idle or response limit | The provider's own error page and code |
fastcgi_read_timeout. A 502 with "upstream prematurely closed connection" in the log points at PHP-FPM killing the worker. Raising the wrong one changes nothing.
Mitigation scoped to one endpoint
fastcgi_read_timeout 300s; once in the http block and move on. That works, but every PHP request on the server now gets five minutes to hang, including ones stuck on a dead external API. During a traffic spike, those slow requests tie up PHP-FPM workers for much longer, and the rest of the site starts queuing behind them.
A narrower option is to raise it only for the path that needs it:
NGINX
# Exact-match location for the long-running endpoint only (illustrative path)
location = /admin/import.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME document_rootfastcgi_script_name;
# Use the same socket or address as your main PHP location
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_read_timeout 300s;
}location ~ \.php$ block contains. Run sudo nginx -t before reloading. On panel-managed servers, the generated Nginx config may get rewritten on the next rebuild, so use the panel's supported include or override mechanism instead of editing generated files directly.
Then line up the PHP side so the layers fail in a sensible order:
INI
; PHP-FPM pool file (location varies by distro, PHP version, and panel)
; Applies to every script in this pool, not just the import
php_admin_value[max_execution_time] = 280
; Hard wall-clock backstop for stuck workers; 0 means disabled
request_terminate_timeout = 290sMoving long jobs out of the request path
- Run it from the command line, where PHP's default
max_execution_timeis unlimited and no web server is waiting. WP-CLI for WordPress, a framework's console command, or a plainphpscript started over SSH all work. - Hand it to a background worker or a cron-driven queue. The web request only creates the job and returns right away. The page then polls for progress.
- Split it into batches. An import that handles a few hundred rows per request, with the browser calling the next batch, never comes close to any timeout. A failure only costs one batch.
- Make the job idempotent where possible (safe to run twice with the same result), so a retry after a timeout cannot double-insert data.
fastcgi_read_timeout, and anything in front of Nginx. Read the status code and the error log line first, because they tell you which layer actually acted. For the Indonesian-language version of the core misconception, see Mengira Naikkan max_execution_time Cukup untuk Hilangkan Error 504 di Nginx.