The Error
504 Gateway Timeout
Nginx is acting as a reverse proxy, and your upstream server โ PHP-FPM, Node.js, Gunicorn, whatever โ didn't respond in time. Nginx sat there waiting, hit its timeout limit, and gave up.
Worth noting: this is not the same as a 502. A 502 means the upstream returned garbage or wasn't reachable at all. A 504 means the connection was established โ the upstream just took too long to finish.
Why This Happens
- A slow database query or external API call is holding up the request โ a 10-second DB query on a dashboard page is a classic culprit
- Your upstream process (PHP-FPM worker, Node process) is overloaded or stuck in a loop
- Nginx's default 60-second timeout is too short for your workload โ think file uploads, CSV exports, or report generation
- PHP's
max_execution_timekills the script before Nginx's timeout fires, causing the upstream to drop mid-request
Step 1 โ Check Nginx Error Logs First
Don't touch any config files yet. Look at what Nginx is actually telling you:
sudo tail -f /var/log/nginx/error.log
You'll typically see something like:
[error] upstream timed out (110: Connection timed out) while reading response header from upstream
That line confirms Nginx timed out waiting for a response header. The upstream started processing but never finished fast enough. That's your smoking gun.
Step 2 โ Increase Nginx Proxy Timeouts
Nginx's default proxy_read_timeout is 60 seconds. For anything doing real work โ generating PDFs, calling third-party APIs, processing large uploads โ 60 seconds disappears fast.
Open your server block config (typically /etc/nginx/sites-available/yoursite or /etc/nginx/conf.d/yoursite.conf):
sudo nano /etc/nginx/sites-available/yoursite
Add these directives inside your location block:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_connect_timeout 60s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
- proxy_connect_timeout โ how long to wait when opening the connection to upstream
- proxy_send_timeout โ how long to wait while sending the request upstream
- proxy_read_timeout โ how long to wait for upstream to respond (this is the one causing your 504)
120s covers most cases. For batch jobs or large file uploads, bump it to 300s. Don't set 600s globally โ more on that in the tips section.
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Step 3 โ Fix PHP-FPM Timeouts (if using PHP)
PHP has its own execution limits. They can kill your script before Nginx's timeout even kicks in. Check /etc/php/8.x/fpm/php.ini:
max_execution_time = 120
request_terminate_timeout = 120
Also update your PHP-FPM pool config (/etc/php/8.x/fpm/pool.d/www.conf):
request_terminate_timeout = 120
Restart to apply:
sudo systemctl restart php8.2-fpm
Step 4 โ Check Upstream App Performance
Bumping timeouts is a band-aid. You still need to know why your upstream is slow.
Is the upstream process even running?
# For PHP-FPM
sudo systemctl status php8.2-fpm
# For Node.js / Gunicorn
ps aux | grep node
ps aux | grep gunicorn
Are all workers saturated? A site handling 50 concurrent requests with only 5 PHP-FPM workers will queue requests until they time out. Check active workers:
# PHP-FPM status (if enabled in pool config)
curl http://127.0.0.1/fpm-status
# General process check
top -bn1 | grep -E "(php|node|gunicorn|uwsgi)"
Workers maxed out? Increase the pool. Edit /etc/php/8.x/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
Step 5 โ Slow Query? Check Your Database
504s that only hit specific pages โ dashboards, reports, export endpoints โ almost always trace back to a slow DB query. Enable MySQL's slow query log to find the offender:
# In /etc/mysql/mysql.conf.d/mysqld.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
sudo systemctl restart mysql
sudo tail -f /var/log/mysql/slow.log
Queries taking 5โ60 seconds? That's your 504. A missing index on a 2-million-row table can turn a 2ms query into a 45-second one. Add an index, rewrite the query, or cache the result.
Verification
After making changes, run through this checklist:
# Validate Nginx config
sudo nginx -t
# Reload
sudo systemctl reload nginx
# Watch error log live
sudo tail -f /var/log/nginx/error.log
# Time the slow endpoint directly
curl -w "\nTime total: %{time_total}s\n" -o /dev/null -s https://yoursite.com/slow-endpoint
If time_total stays under your timeout and the error log goes quiet, you're done.
Quick Tips
- Scope your timeouts โ don't crank them up globally. Apply long timeouts only to the specific
locationblocks that need them (/api/export,/upload). A 300s global timeout is a slow-loris DoS waiting to happen. - Offload long jobs to a queue โ for anything that genuinely takes minutes, don't fight the timeout. Push the work to a background queue (Redis + a worker), return a job ID immediately, and let the client poll for results. No more timeouts.
- fastcgi_read_timeout for FastCGI โ using
fastcgi_passinstead ofproxy_passfor PHP? You need this directive, notproxy_read_timeout:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 120s;
include fastcgi_params;
}

