504 Gateway Timeout tells you a duration, not a fault
The difference between a 502 and a 504 is patience. In both cases a proxy asked your site for a page and did not get one it could use. In the 502 case, something came back and it was wrong or nothing at all. In the 504 case, nothing came back yet, and the proxy stopped waiting.
That word, yet, is the entire article. Your application is probably still working on that request right now. It may finish. It may write to the database, send an email, charge a card, and then discover that nobody is listening.
The number is in a config file, not in the error
A timeout is a decision somebody made, and on a normal stack several people made it independently. The one that fires first wins, and it is rarely the one you would guess.
| Where | Setting | Common default |
|---|---|---|
| Cloudflare (free and Pro) | Origin response timeout | 100 seconds, and not configurable on those plans |
| nginx to PHP-FPM | fastcgi_read_timeout | 60 seconds |
| nginx to an HTTP backend | proxy_read_timeout | 60 seconds |
| PHP itself | max_execution_time | 30 seconds on most shared plans |
| PHP-FPM pool | request_terminate_timeout | often unset, which means no limit |
Read that table in order and a familiar pattern appears. PHP is told to stop after thirty seconds. nginx is told to wait sixty. So a script that hangs on a database query, which does not count against max_execution_time because the time is spent waiting on I/O rather than executing, will sail past the thirty and get cut off at the sixty. You get a 504, PHP's own limit never fired, and the error log has nothing in it.
That last detail is why 504s feel unfair. The component that gave up is not the component that was slow, and it is not the component that writes to the log you are reading.
What to do when you do not own the stack
If you manage client sites on shared hosting, most of that table is out of reach. You cannot edit nginx. You may be able to raise max_execution_time from a control panel, which will make things worse: a longer PHP limit under a fixed proxy limit means the request still gets cut at sixty seconds, but now it does so after doing more work.
The useful move is to find out what is slow, and the fastest way is usually to ask for less.
curl -s -o /dev/null -w 'connect %{time_connect} ttfb %{time_starttransfer} total %{time_total}\n' \
https://example.com/
Run that against a static file, then the homepage, then whichever admin page triggered the complaint. If time_connect is fine everywhere and ttfb only climbs on dynamic pages, the network is innocent and you are looking at the application. If a static file is also slow, stop reading about timeouts and look at the host.
On WordPress specifically, three things produce most of the 504s that arrive on a Monday morning: a plugin doing an outbound HTTP call with no timeout of its own, an autoloaded options table that has grown to several megabytes, and a scheduled task running inside a page request rather than beside it. The third one has its own article coming, because wp-cron deserves the blame it gets.
The thing a 504 does to your monitoring
Most checkers have a timeout of their own, and it is almost always shorter than your proxy's. Ten seconds is a common default. That produces a strange and specific failure: your monitor reports the site as down while every visitor gets a page, slowly. Or the reverse, if your checker is patient and your proxy is not.
The fix is not to make your checker wait longer. It is to record the response time on every check rather than only the failures, so that the graph shows the climb from four hundred milliseconds to eight seconds over three weeks. A tool that only stores outages cannot show you that, and by the time it has something to store, the client has already called.
That is the same argument as the one for intermittent 502s, which is covered here along with the commands that tell a dead process apart from a full one.
Two cases that look like a timeout and are not
A page that eventually loads after ninety seconds is not a 504, it is a page that works and should not. Nobody is waiting ninety seconds, so the outcome for the visitor is identical to an error, but your status-code monitoring will report 200 all day.
And a page that returns quickly with a partial render, missing its stylesheet or its main content block, is also 200. Response codes have nothing to say about either. If you want to see what a checker looks like when it reads the body instead of just the header, try an address here.