The Scenario: When 60 Seconds Isn't EnoughA 504 Gateway Timeout in the browser is just a symptom. The real story lives in your Nginx error logs. I recently ran into this while building a reporting feature that exports 50MB PDF files. Small reports worked instantly, but as soon as the dataset grew to 5,000+ rows, the request would hang for exactly 60 seconds and then die.
A quick look at /var/log/nginx/error.log showed the problem:
[error] 1234#0: *567 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 1.2.3.4, server: example.com, request: "POST /api/export HTTP/1.1", upstream: "http://127.0.0.1:8000/api/export"
This means Nginx connected to your backend—whether that's PHP-FPM, Gunicorn, or Node—and sent the request. However, the backend didn't send a single byte of the response headers back before the timer ran out.
Analysis: The 60-Second WallNginx is patient, but only to a point. By default, it waits 60 seconds for an upstream response. If your app is busy processing a massive database query or fetching data from a sluggish third-party API, it will hit this wall. The "110: Connection timed out" error is a specific network signal. It tells you the internal connection between Nginx and your application stayed open but silent for too long.
The Immediate Fix: Extending TimeoutsYou need to give Nginx more breathing room. The specific directive you need depends on how Nginx talks to your app. I usually set these to 300 seconds (5 minutes) for heavy-duty endpoints.
For Reverse Proxy (Node.js, Gunicorn, Go)If you use proxy_pass, update your location block. Increasing all three timeout values ensures the connection doesn't drop during any phase of the request.
location /api/ {
proxy_pass http://localhost:8000;
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
}
For PHP-FPM (FastCGI)PHP setups use the FastCGI protocol. You'll need the fastcgi_read_timeout directive here. This is common for long-running Laravel or WordPress scripts.
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 300s;
}
For Python/UWSGI```
location / { include uwsgi_params; uwsgi_pass unix:/tmp/app.sock; uwsgi_read_timeout 300s; }
Reload Nginx to apply the changes:
sudo nginx -t && sudo systemctl reload nginx
## The Missing Link: Backend ConfigChanging Nginx is only one side of the coin. If your backend has its own 30-second timeout, it will kill the process before Nginx even finishes waiting. You must sync these limits.
### Updating PHP-FPMEdit your `php.ini` to allow longer scripts:
max_execution_time = 300
Also, check `/etc/php/8.2/fpm/pool.d/www.conf` for this line:
request_terminate_timeout = 300
### Updating Gunicorn (Python)Gunicorn defaults to a tiny 30-second timeout. If you don't change this, Nginx will report a 'Bad Gateway' or timeout when Gunicorn kills the worker. Start it like this:
gunicorn --timeout 300 myapp.wsgi:application
## Verification: Test the New LimitDon't just trust the config. Use `curl` to see exactly how long your server holds the connection:
time curl -I http://example.com/api/heavy-task
Watch the 'real' time output. If it goes past 60 seconds and returns a `200 OK`, you've fixed it. If it still fails at the 300-second mark, your task is simply too slow for a standard web request.
## Best PracticesBumping timeouts is a band-aid. Users hate staring at a loading spinner for five minutes. For heavy tasks, move the logic to a background worker like Celery or Redis Queue. Let the user trigger the job, receive a 'Job Started' message, and poll for the result later. It’s more complex to build but much more reliable.

