Short answer: a 504 Gateway Timeout error means a proxy or gateway (nginx, Apache, a load balancer or a CDN) waited for the server behind it and gave up before an answer arrived. The backend is usually alive but too slow. Visitors can wait and reload once. Site owners should find the slow request before raising any timeout.
# Which request timed out, and where?
sudo grep 'upstream timed out' /var/log/nginx/error.log | tail -n 5
# How long does the page take? (first byte = time to the response headers)
curl -s -o /dev/null -w 'status %{http_code} first byte %{time_starttransfer}s total %{time_total}s\n' \
https://example.com/slow-pageIf the first byte arrives after almost exactly 60 seconds with status 504, you are hitting a proxy’s default timeout, and the question is why the backend needs that long. The rest of this page answers it.
What does a 504 error mean?
RFC 9110 defines 504 as a server that, “while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request”. The gateway is whatever sits in front: nginx in front of PHP-FPM or a Node app, Apache with mod_proxy, a load balancer, or a CDN. It forwarded the request, started a clock, and the clock ran out.
That has a consequence people miss: the backend may still be working on the request after the 504, and may finish it. The visitor sees an error for an order, an import or a password reset that actually went through.
The wording differs by software. nginx and HAProxy print 504 Gateway Time-out with a hyphen, Apache prints Gateway Timeout: The gateway did not receive a timely response from the upstream server or application, and browsers show the proxy’s page as it is. IIS with Application Request Routing is the exception that reports a backend timeout as 502.3 rather than 504.
504 vs 502 vs 408 vs Cloudflare 524
| Error | Who gave up | Waiting for | Look at |
|---|---|---|---|
| 504 Gateway Timeout | Your proxy: nginx, Apache, a load balancer | The backend's response | Slow queries, busy workers, the proxy timeout |
| 502 Bad Gateway | Your proxy | Nothing: the answer was broken or the connection failed | Is the backend running and reachable? |
| 408 Request Timeout | The web server | The client to finish sending its request | The client's connection, slow uploads |
| Cloudflare 524 | Cloudflare | Your origin's response, 125 seconds by default | The same slow request, past Cloudflare's limit |
| Cloudflare 522 | Cloudflare | A TCP connection to the origin at all | Firewall, a stopped server, routing |
A slow request can also end as a 502: when the backend enforces its own limit first, it kills the request and closes the connection, and the proxy reports a broken answer rather than a late one. Both codes for the same slow page mean the same problem.
How do I fix a 504 error as a visitor?
- Wait a minute, then reload once. A 504 often comes from a short spike. Reloading again and again adds requests to the queue that caused it.
- Do not resubmit a payment or an order straight away. The server may have finished the request after the proxy gave up. Check your e-mail or the account page for a confirmation first.
- Ask for less. If the error appears on a large export, report or search, a shorter date range or fewer results often fits inside the timeout.
- Rule out your own proxy. If every site times out, the gateway is on your side: a VPN, a corporate proxy or a filtering product. Try without it.
Site owner: which timeout fired?
Every layer between the visitor and your code has its own clock. The shortest one decides what the visitor sees, and only some of them produce a 504:
| Layer | Setting | Default | Visitor gets |
|---|---|---|---|
| nginx in front of an app | proxy_read_timeout | 60 s | 504 Gateway Time-out |
| nginx in front of PHP-FPM | fastcgi_read_timeout | 60 s | 504 Gateway Time-out |
| Apache mod_proxy | ProxyTimeout, else Timeout | 60 s | 504 Gateway Timeout |
| HAProxy | timeout server | None; HAProxy warns at start-up | 504 Gateway Time-out |
| Cloudflare | Proxy Read Timeout | 125 s; only Enterprise can raise it | 524 |
| PHP | max_execution_time | 30 s (script time only) | 500 from a PHP fatal error |
| PHP-FPM | request_terminate_timeout | 0 (off) | 502, the worker is killed |
| gunicorn | --timeout | 30 s | 502, the worker is killed |
Defaults are from each project’s documentation; Cloudflare’s documentation gives 125 seconds, where many older guides still say 100. Your config may differ, so check what is actually loaded: sudo nginx -T | grep -n timeout for nginx, sudo apache2ctl -S plus a grep for Timeout in /etc/apache2/ for Apache.
Reading the log line
2026/10/21 09:14:03 [error] 2214#2214: *8841 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 198.51.100.7, server: example.com,
request: "GET /admin/export.csv HTTP/2.0", upstream: "http://127.0.0.1:3000/admin/export.csv"while reading response header from upstream means the backend accepted the request and had not started its answer when the 60 seconds were up. That is the common case: the code is slow. while connecting to upstream means the backend never accepted the connection, which points at the network or a firewall instead. The nginx 504 guide covers every variant of this line and the nginx settings in detail. Apache logs the same event as (70007)The timeout specified has expired: AH01102: error reading status line from remote server.
Measure where the time goes
curl’s -w option prints the timing of each step of a request. This is the output of one run against example.com with curl 7.88.1 on Debian 12:
curl -sI -o /dev/null -w 'status %{http_code}\ndns %{time_namelookup}s\nconnect %{time_connect}s\ntls %{time_appconnect}s\nfirst byte %{time_starttransfer}s\ntotal %{time_total}s\n' https://example.comstatus 200
dns 0.004624s
connect 0.005815s
tls 0.021307s
first byte 0.030717s
total 0.030750sEach value is the time since the start, so the gaps tell the story. DNS, connect and TLS take milliseconds on a healthy path; the step from TLS to the first byte is the time the server spent producing the answer, and that is the step a 504 is about. For a real check, drop -I so the request is a normal GET, and point it at the slow URL.
Then take the proxy out of the picture. On the server, ask the backend directly, with a generous limit so curl does not give up first:
curl -s -o /dev/null --max-time 300 \
-w 'status %{http_code} first byte %{time_starttransfer}s\n' \
http://127.0.0.1:3000/admin/export.csvIf the backend alone needs 95 seconds, no proxy setting will make the page fast; the work behind it is the problem. If it answers in a second directly but times out through the proxy, look at the path between them: a wrong address, a firewall, or a worker pool that is full only under real traffic.
To check a page from outside your own network, the HTTP header checker requests it from our server and shows the status code and headers it got back.
Why is the backend slow?
Long database queries
The most common cause on a site that used to be fast. A table grows, an index is missing, and a query that took 50 ms now scans millions of rows. Turn on the slow query log of MariaDB or MySQL, or log_min_duration_statement in PostgreSQL, and run EXPLAIN on the worst query. type: ALL in MariaDB or Seq Scan on a large table in PostgreSQL usually means an index is missing. Adding one is often the whole fix.
Heavy work inside the request
Exports, imports, PDF and image generation, sitemap builds and bulk e-mail do not belong in a request a person waits for. Neither do scheduled tasks: WordPress runs WP-Cron through requests to wp-cron.php triggered by page views, so a heavy job holds a PHP worker every time it runs. Set define( 'DISABLE_WP_CRON', true ); in wp-config.php and call wp-cron.php from the system crontab instead.
A full worker pool
Each backend handles a fixed number of requests at once: PHP-FPM up to pm.max_children (5 in the pool Debian and Ubuntu ship), gunicorn as many as it has workers. When all are busy, new requests wait in a queue, and the wait counts against the proxy’s timeout. A few slow pages can therefore make fast pages time out too. PHP-FPM logs server reached pm.max_children setting when it happens. Node runs your JavaScript on one thread, so one request doing heavy synchronous work blocks all the others until it is done.
The network or DNS behind the backend
- Outside calls without a timeout. A request that calls a payment API, a mail server or another service waits as long as that service takes. Give every HTTP client in your code an explicit timeout.
- A broken resolver. If the server’s DNS is slow or down, every outside call first waits for name resolution to time out; see temporary failure in name resolution.
- A backend that moved. nginx resolves a host name in
proxy_passonce, when it starts or reloads. If the backend’s IP changes, nginx keeps connecting to the old one, and with packets dropped that ends inwhile connecting to upstreamtimeouts until the next reload.
The server itself is overloaded
If every page is slow at once, look at the machine: CPU at 100 %, high steal time, or heavy swapping slow every request down. VPS high CPU usage walks through finding the process and telling the cases apart.
Fix the slow part first, then set the timeouts
Raising a timeout only moves the failure. With a 300-second timeout, a request that needs 400 seconds still fails, and meanwhile it holds a worker that other visitors are waiting for. Work through it in this order:
- Make the request fast: an index, a cache for pages that are the same for everyone, pagination for long lists.
- Move long jobs out of the request: answer at once with
202 Acceptedand a job ID, run the job in a background worker (a queue in your framework, or a small systemd service), and let the page poll for the result or send an e-mail when it is ready. - Then raise timeouts only where a route legitimately runs long, and keep them in order: the application’s own limit shortest, the proxy’s longer, the CDN’s longest. The innermost layer then stops a runaway request with a clear error in its own log.
location /admin/export/ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 300s;
}ProxyPass /admin/export/ http://127.0.0.1:3000/admin/export/ timeout=300
ProxyPassReverse /admin/export/ http://127.0.0.1:3000/admin/export/sudo nginx -t && sudo systemctl reload nginx
sudo apache2ctl configtest && sudo systemctl reload apache2For PHP, raise max_execution_time for that script and fastcgi_read_timeout above it; the nginx 504 guide shows the three PHP limits side by side.
When the server is simply too small
Sometimes the code is fine and the server is not. Measure first: htop during the slow period shows whether one or two cores sit at 100 % while the rest idle, or whether memory and swap run out. Work that leans on one or two cores fits a Hi-CPU KVM plan on AMD EPYC, which carries fewer vCores per plan. A site that runs out of PHP workers because each one needs memory wants a larger plan with more RAM, such as a bigger Standard KVM tier, which carries the most vCores per euro.
Frequently asked
What does a 504 Gateway Timeout error mean?
A server acting as a gateway or proxy, such as nginx, Apache, a load balancer or a CDN, forwarded the request and did not get an answer from the server behind it in time. The backend is usually running but too slow, for example because of a long database query or a full worker pool.
How long does it take before a 504 Gateway Timeout appears?
It depends on the proxy. nginx waits 60 seconds by default (proxy_read_timeout and fastcgi_read_timeout), Apache's ProxyTimeout falls back to Timeout, also 60 seconds, and Cloudflare waits 125 seconds by default before it shows its own error 524 instead. A first byte at almost exactly one of those values points at that layer.
Should I just increase the timeout to fix a 504?
Only for routes that legitimately run long, and only after making the request faster or moving the work to a background job. A higher timeout keeps a worker busy for longer, so other visitors queue behind it, and behind Cloudflare anything above 125 seconds still ends in a 524.
Is a 504 error my fault as a visitor?
No. It happens between servers. Wait a minute and reload once, and do not resubmit a payment or an order straight away, because the server may have completed it after the proxy gave up. If every site times out, check your VPN or proxy.