nginx 504 Gateway Timeout Fix Guide: From proxy_read_timeout to Upstream Timeouts
The deploy went fine, but as soon as you hit a heavy report API or an LLM response call, a 504 Gateway Timeout appears at exactly 30 or 60 seconds. Sound familiar? A 504 means nginx forwarded the request to the upstream (app server) just fine, then cut the connection because no response came back in time. The key point: this is a timeout problem, not a connection failure.
This post walks through diagnosing where the 504 is happening, fixing it by pasting timeout directives in the right place, and aligning timeouts in multi-hop setups such as ALB, nginx, and gunicorn.
First, Tell 504 and 502 Apart Correctly
504 and 502 look similar on the surface but have completely different causes. One-line summary: 504 = the upstream is alive but did not respond in time; 502 = the upstream is dead or the response is broken.
| Aspect | 504 Gateway Timeout | 502 Bad Gateway |
|---|---|---|
| Meaning | Timeout (slow response) | Connection failure / broken response |
| When it happens | After a fixed delay (30s / 60s) | Immediately or mid-processing |
| Typical log | upstream timed out (110: Connection timed out) | connect() failed (111: Connection refused) |
| First suspects | Slow queries / external APIs, proxy_read_timeout | App server down, worker crash |
If you suspect a 502, the cause is different—see the [nginx 502 Bad Gateway troubleshooting guide]. This post focuses on timeouts.
Diagnose by Location: Client → nginx → Upstream
Do not blindly raise proxy_read_timeout. First figure out where the connection was cut.
Step 1 — Measure wait time from the client
curl -w "@-" -o /dev/null -s https://example.com/slow-api <<'EOF'
time_connect: %{time_connect}s
time_starttransfer: %{time_starttransfer}s
time_total: %{time_total}s
EOFIf time_total cuts off at exactly 60 seconds (or 30 seconds), a timeout is almost certain.
Step 2 — Check nginx error.log
tail -f /var/log/nginx/error.logIf you see upstream timed out ... while reading response header from upstream, you hit nginx's read timeout.
Step 3 — Call the upstream directly
curl -w "%{time_total}\n" -o /dev/null -s http://localhost:8000/slow-apiIf it is still slow or gets cut off here, the culprit is the upstream (app server), not nginx. Raising nginx timeouts alone will not help.
Copy-Paste nginx Timeout Directives
The usual 504 culprit is proxy_read_timeout (default 60s). Put it in a server or location block and reload.
location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 5s; # TCP
proxy_send_timeout 60s; # nginx → Request
proxy_read_timeout 300s; # reply (504 !)
}For FastCGI setups such as PHP-FPM, the directives are different.
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_connect_timeout 5s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 300s; # FastCGI 504
}After changing the config, always syntax-check and reload.
nginx -t && nginx -s reloadPractical tip: Do not set 300s for the entire site. Split slow endpoints into their own
locationand give those a long timeout; leave the rest at the default. If large uploads are getting cut off, it may be a request-size limit rather than a timeout—see the [nginx 413 troubleshooting guide] as well.
When the Upstream Is the Real Culprit
The most common mistake is raising only the nginx timeout and leaving the upstream as-is, so you still get 504s. You have to raise the app server timeout too.
| App server | Directive | Default | Notes |
|---|---|---|---|
| gunicorn | --timeout | 30s | Classic 504 cause; worker is killed if it does not respond |
| php-fpm | request_terminate_timeout | 0 (unlimited) | Too few pm.max_children → workers queue up → 504 |
| uWSGI | harakiri | none | Worker is force-killed on exceed |
For gunicorn, raise it like this:
gunicorn app:app --workers 4 --timeout 300If php-fpm does not have enough workers, requests pile up in the queue and you get 504s. Increase pm.max_children to match your traffic.
Aligning Timeouts Across Multi-Hop Proxies
In microservice and container setups, multi-hop chains such as ALB → nginx-ingress → nginx → gunicorn are common, and timeout-alignment issues have become really frequent. The rule is simple: the outer hop must be longer.
ALB idle timeout (60s+) ≥ nginx proxy_read_timeout ≥ gunicorn --timeoutIf this order is inverted—for example, if the ALB idle timeout (60s) is shorter than nginx (300s)—the ALB will drop the connection while nginx is still waiting, and you get a 504. This has been showing up a lot recently with streaming backends that proxy LLM APIs. The longer the response, the more you should start by checking the outermost LB's idle timeout.
Mapping Log Messages to Causes
| Log message | Cause | What to fix |
|---|---|---|
upstream timed out ... while reading response header | Response is too slow | proxy_read_timeout + upstream processing speed |
upstream timed out ... while connecting to upstream | Connection delay / upstream overload | proxy_connect_timeout, worker count |
| Nothing in the nginx logs | Cut off in front of nginx (LB) | ALB/ELB idle timeout |
30-Second Checklist
- Use
curl -wto see at how many seconds it cuts off (30/60/300?) - In error.log, check where the
timed outmessage appears (readingvsconnecting) - Call the upstream directly with
curl localhost:8000to identify the real culprit - Split slow endpoints into their own
locationand adjustproxy_read_timeout - Also raise the upstream timeout (e.g. gunicorn
--timeout) - Confirm the order ALB ≥ nginx ≥ app server
nginx -t && nginx -s reload
References: Official Docs
The primary source for the behavior, settings, and errors covered here is the official documentation below. Check it for version-specific options and exact behavior.
FAQ
Q. Can I just raise the timeout indefinitely? A. No. Raising timeouts is first aid only. The root cause is usually a slow DB query or a delayed external API. A longer timeout means nginx connections stay occupied longer, so throughput drops. Fix the actual response time with query indexes, caching, and async processing.
Q. I get a 504 but nothing shows up in the nginx logs. A. It was probably cut off in front of nginx (ALB/ELB, CDN). First check whether the LB idle timeout is shorter than nginx.
Q. I get 504s on WebSocket/SSE.
A. Streaming and long-polling connections keep sending data after the response headers, so set proxy_read_timeout long enough (very long for SSE). For WebSocket, also confirm proxy_http_version 1.1 and that the Upgrade/Connection headers are forwarded.
Q. How is this different from the 502 post? A. 504 is a timeout problem: the upstream is alive but slow. 502 is a connection problem: the upstream is dead or the response is broken. For 502, see the [nginx 502 Bad Gateway troubleshooting guide].
Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us
This article has no recorded human check (date and scope) yet.
Comments
Be the first to comment.