Common causes
- A slow database query, missing index or locked table holds the request
- The script calls an external API that hangs or responds slowly
- Long imports, exports or report generation run inside a web request
- All PHP-FPM workers are busy, so new requests wait in the queue until the proxy times out
- Timeouts are inconsistent between layers, for example fastcgi_read_timeout lower than max_execution_time
- A CDN limit (Cloudflare returns 524 after about 100 seconds) is shorter than the request
How to fix it
- Find the slow request. Match the timestamp of the 504 in /var/log/nginx/error.log with the access log. Note the URL and whether every request or only one endpoint times out.
- Profile the slow part. Enable the MySQL slow query log (slow_query_log = 1, long_query_time = 1) and the PHP-FPM slowlog (request_slowlog_timeout = 5s) to see which query or function is stuck.
- Fix queries and external calls. Add indexes for slow queries and set explicit timeouts on outbound HTTP calls (for example CURLOPT_TIMEOUT in PHP) so one slow API cannot hang the page.
- Move long jobs off the request. Run imports, exports and bulk emails through a queue worker or cron and let the page poll for status instead of waiting.
- Align timeouts where long requests are expected. Set fastcgi_read_timeout (or proxy_read_timeout) in Nginx, max_execution_time in PHP and request_terminate_timeout in the FPM pool to consistent values, then reload both services.
Nginx + PHP-FPM timeouts
# nginx location ~ \.php$
fastcgi_read_timeout 120s;
# php.ini
max_execution_time = 120
# php-fpm pool
request_terminate_timeout = 130s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log How to stop it happening again
- Keep the slow query log and FPM slowlog enabled to catch regressions early
- Put timeouts on every external API call
- Run heavy work in background queues, not in page requests
- Track response times so slow endpoints are fixed before they hit the limit