The browser spins, and after a few seconds you get a blank page or the message 504 Gateway Timeout. The PHP log shows no errors, the server CPU is low, but the request never completes. This situation almost always means one thing: one of the links in the request chain got impatient before the response was ready and closed the connection. Your job is to find that link, not to blindly raise all timeouts.
Which layer the 504 error comes from
The user's request passes through several intermediaries before reaching your code. Each has its own independent timeout, and whichever one expires first produces the same message:
- The user's browser (internal timeout, usually long)
- A CDN or front-end proxy like Cloudflare
- The front-end web server (Nginx or Apache) acting as a reverse proxy
- The PHP process manager, such as PHP-FPM
- The application code and, at the end, the database or an external API
The key point is that the 504 message only comes from intermediary layers. If PHP itself dies or runs out of memory, you'll see a 500 or 502 error, not a 504. So seeing a 504 means your code is probably alive, it's just responding late.
The difference between 504, 502, and 524
502 means the intermediary couldn't connect to the upstream server; the connection wasn't established. 504 means the connection was established but the response didn't arrive within the allowed window. Cloudflare uses the number 524 for this same situation of its own, and its default is 100 seconds. If you see the number 524 in the Cloudflare log and there's no trace of the request in the Nginx log, it means the request never even reached your server, or the response wasn't ready before 100 seconds. This distinction is half the troubleshooting work.
Which timeout expires first
You need to determine the actual order from the logs, but the common default order is as follows:
| Layer | Parameter | Common default value |
|---|---|---|
| Cloudflare | Proxy timeout | 100 seconds |
| Nginx (proxy) | proxy_read_timeout | 60 seconds |
| PHP-FPM | request_terminate_timeout | Usually unlimited or 30 seconds |
| PHP | max_execution_time | 30 seconds |
| MySQL | wait_timeout | 28800 seconds |
With this table, the first thing that kills the request is PHP: at second 30 it kills the script, and if it doesn't have a clean output, Nginx waits until 60 seconds and then returns a 504. That means the user waits 60 seconds to see an error whose root cause was at second 30. Here's the mistake people make: they raise max_execution_time to 300, but don't touch proxy_read_timeout. The result is that the error doesn't change, it just arrives later, and the PHP log fills up with Maximum execution time of 30 seconds exceeded that is no longer there.
Finding the bottleneck link with logs
Before making any changes, you need to understand where the request is stuck. Look at three places at the same time:
tail -f /var/log/nginx/error.log
tail -f /var/log/php*-fpm.log
tail -f /var/log/nginx/access.log | grep " 504 "
In the Nginx access log, add the $request_time and $upstream_response_time fields. If upstream_response_time is a number close to your timeout, the problem is in PHP or the database. If it's a dash or zero, the request never reached the upstream and the problem is in Nginx itself or the network.
log_format timed '$remote_addr $request_time $upstream_response_time "$request" $status';
To see the slowest requests, sort the log by time. A simple script is enough, but if you don't want to spend time doing it manually, free webmaster tools for quickly checking server response and headers will shorten the work.
When the problem is in the database
The most common root cause of 504 on WordPress sites is a query without an index. By enabling the slow query log in MySQL, find out which query takes more than one second:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
If you see a query with Rows_examined above several hundred thousand, the problem isn't the timeout; the problem is the index, and no timeout will solve it. Here's the mistake people make: instead of adding an index, they raise the timeout and make the site slower for everyone, because every slow request now occupies resources for longer.
Setting timeouts correctly in order
The rule is simple: each layer's timeout must be larger than the inner layer's, otherwise the outer layer cuts off first and gives a misleading message. A logical order:
- PHP:
max_execution_time = 120 - PHP-FPM:
request_terminate_timeout = 130 - Nginx:
proxy_read_timeout 150s;andfastcgi_read_timeout 150s; - Cloudflare: can be increased on higher plans, but keeping it under 100 seconds is more logical
Also mention the cost of this: every extra second of timeout occupies one more PHP-FPM process. If you don't also raise pm.max_children, a few slow requests are enough to fill the queue and make the site return 504 for everyone. A long timeout without enough capacity moves the problem from one user to all users.
Recommended approach for high-traffic sites
If traffic is high, instead of lengthening the timeout, move the heavy work out of the request path. Run heavy reports with cron and a processing queue, and put the result in the database or cache. This way the user's page always responds quickly and the timeout never reaches the execution stage. For sites that need more resources, a dedicated server allows more precise tuning of these parameters, but on shared hosting usually only some of these values are available to you.
When no timeout solves the problem
I've seen three cases in practice many times where tuning the timeout is useless:
- A slow external API that has no timeout of its own. Here you must set a timeout in the code, not on the server.
- An infinite loop in the code that never ends. The timeout only kills it, but the root cause remains.
- A network problem between the server and an external database. Check with
mtrortraceroute.
In the first case, a short timeout in the code is the best approach: if the API doesn't respond within 5 seconds, show the cached value. The user sees a slightly outdated page, but doesn't see an error page.
Frequently asked questions
Why does the 504 error only appear for some users?
Because it usually depends on the network path or cache. A user served from the CDN cache never reaches your server and doesn't see the error. A user whose request results in a cache miss travels the full path, and if the server is busy at that moment, they get a 504. That's why you should filter the log by IP and request path, not just count the total number of errors.
Does raising max_execution_time fix the 504 error?
Only if PHP really was cut off earlier than the other layers, and the outer layers also have proportionally larger timeouts. If Cloudflare cuts off at 100 seconds, raising max_execution_time to 300 only makes the process continue pointlessly and the user still sees an error. First fix the order of the timeouts, then raise the values.
How do I tell whether the problem is the server or the site's code?
Create a simple PHP file with the content <?php echo "ok"; ?> and request it directly. If this file responds quickly but the main page returns a 504, the problem is in the code or the database. If even that simple file is slow, the issue is in the web server, PHP-FPM, or server resources. This two-minute test determines the direction of your troubleshooting.
What is DNS's role in the 504 error?
Usually none. A 504 means the TCP connection was established and the response didn't arrive; if DNS had a problem, you'd see a connection error, not a 504. The only exception is when your record points to a proxy or intermediary service that is itself slow. To make sure the records and path are correct, do a DNS and network check and then move on to the timeouts.
The next step is clear: enable the Nginx log with $upstream_response_time, reproduce the error once, and see where the number stops. That number shows you the guilty layer. If after this investigation you need hosting that gives you access to these parameters, ServerNet's Linux hosting provides this capability; but until you've found the root cause, changing hosting only moves the error around.
Comments 0
No comments yet — be the first!