504 Gateway Timeout — how to fix
504 Gateway Time-out
nginx
upstream timed out (110: Connection timed out) while reading response header from upstream
Quick fix: Check the proxy error log (sudo tail -f /var/log/nginx/error.log) for upstream timed out. Make the slow request faster or move it to a background job; if it legitimately takes long, raise proxy_read_timeout 300s; (nginx) and the app server's own timeout (e.g. gunicorn --timeout 300).
What it means
A 504 is generated by whatever sits in front of your application. The proxy connected (or tried to connect) to the upstream server and gave up waiting. Compare:
- 502 Bad Gateway – the upstream refused the connection, crashed, or sent garbage.
- 504 Gateway Timeout – the upstream didn't answer within the timeout.
Find out which hop timed out first. With nginx the error log says it explicitly:
sudo tail -n 50 /var/log/nginx/error.log
# upstream timed out (110: Connection timed out) while reading response header from upstream,
# upstream: "http://127.0.0.1:8000/report"
while connecting to upstream means the app is unreachable; while reading response header means it's reachable but slow.
Cause 1: the request really is slow
Large reports, exports, unindexed database queries, or calls to a slow third-party API. Time it directly against the app, bypassing the proxy:
curl -o /dev/null -s -w '%{http_code} %{time_total}s\n' http://127.0.0.1:8000/report
Fix 1: make it faster or asynchronous
- Add missing database indexes (check with
EXPLAIN ANALYZE). - Add timeouts to outbound HTTP calls so one slow dependency doesn't hang the worker.
- Move long work to a queue (Celery, Sidekiq, BullMQ) and return
202 Acceptedwith a job ID the client can poll.
Cause 2: proxy timeout lower than the job needs
nginx defaults to 60 seconds for proxy_read_timeout. If the endpoint legitimately takes longer, raise it for that location only:
location /api/export {
proxy_pass http://127.0.0.1:8000;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
For PHP-FPM use fastcgi_read_timeout 300s;; for uWSGI uwsgi_read_timeout 300s;. Then:
sudo nginx -t && sudo systemctl reload nginx
Cause 3: the app server kills or queues the request
Raising nginx's timeout doesn't help if the app server has its own, shorter limit or all workers are busy:
# gunicorn: default worker timeout is 30s
gunicorn app:app --workers 4 --timeout 300
# PHP: max_execution_time in php.ini and request_terminate_timeout in the FPM pool
If every worker is stuck on slow requests, new requests queue until the proxy gives up. Add workers, or use async workers (-k uvicorn.workers.UvicornWorker, gevent).
Cause 4: upstream unreachable (firewall, wrong address)
while connecting to upstream with a 504 usually means packets are dropped rather than rejected: a security group, firewall, or a container IP that changed. Test from the proxy host:
curl -v --max-time 5 http://10.0.1.15:8000/health
In Docker, proxy to the service name on a shared network instead of a hard-coded container IP.
Cause 5: cloud load balancer / CDN limits
- AWS ALB: idle timeout defaults to 60s (Load balancer → Attributes → Idle timeout).
- Cloudflare: returns 524 after 100s on non-Enterprise plans; it can't be raised, so long work must be asynchronous.
- Kubernetes ingress-nginx: set annotations
nginx.ingress.kubernetes.io/proxy-read-timeout: "300".
Every hop in the chain needs a timeout at least as long as the one behind it.
Last updated 2026-11-04.