Fix Nginx 504 Gateway Timeout Error

intermediateโšก Nginx2026-03-18| Nginx 1.18+ as reverse proxy in front of PHP-FPM, Node.js, Gunicorn, or any upstream app server. Linux (Ubuntu 20.04/22.04, Debian, CentOS).

Error Message

504 Gateway Timeout
#nginx#timeout#gateway#proxy

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_time kills 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 location blocks 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_pass instead of proxy_pass for PHP? You need this directive, not proxy_read_timeout:
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_read_timeout 120s;
    include fastcgi_params;
}

Related Error Notes