Error 504 · server
504 Gateway Timeout
The proxy in front gave up waiting for the backend to answer — your code is running, it is just taking too long.
Also seen as: Gateway Timeout · 504 error · request timed out
What it actually means
A 504 is a 502’s patient cousin. Same two-server setup: a proxy in front, an application behind. The difference is that with 502 the backend failed, and with 504 the backend simply never finished in time.
That is a useful distinction. Your code is probably not broken. It is slow, or it is stuck waiting for something else that is slow — a database query, an external API, a large file operation.
The timeout is usually 30 to 60 seconds. Cloudflare’s is 100 seconds on free plans.
Why it happens
A database query with no index
The single most common cause. A query that ran fine over 1,000 rows takes 40 seconds over 500,000. Nothing changed in your code — the table just grew past the point where a missing index mattered.
An external API that stopped responding
Your page calls a third-party service — a payment provider, a weather feed, a social widget. That service goes slow. Without a timeout, your page waits for it, and your visitors wait too.
One slow third party can take your whole site down. This is worth designing against before it happens.
Work that should not be in a web request
Bulk imports, report generation, sending hundreds of emails, resizing large images. These belong in a background job, not in the seconds a visitor is prepared to wait.
A loop that does not terminate
A while that never satisfies its condition, or recursion without a base case. The request runs until something kills it.
How to fix it
1. Find the slow query first
This is the cause most of the time, so start here.
On WordPress, add define('SAVEQUERIES', true); to wp-config.php and install Query Monitor — it lists every query with its duration. Anything over ~200ms deserves attention.
Then check what the query is actually doing:
EXPLAIN SELECT * FROM orders WHERE customer_email = '[email protected]';
If the type column says ALL, it is scanning the entire table. Add an index:
CREATE INDEX idx_customer_email ON orders (customer_email);
A single index routinely turns a 40-second query into a 5-millisecond one.
2. Put a timeout on every outbound call
Never let a third party decide how long your page takes:
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3, // total
CURLOPT_CONNECTTIMEOUT => 2, // connection only
]);
$response = curl_exec($ch);
if ($response === false) {
$response = get_cached_fallback(); // degrade, do not hang
}
Three seconds is generous for an API call. Cache the result so you are not calling it on every page load.
3. Move long work into the background
If something genuinely takes minutes, it cannot live in a request. Write the job to a table, return immediately, and process it from a scheduled task:
$db->insert('jobs', ['type' => 'import', 'payload' => $csv, 'status' => 'pending']);
header('Location: /import/queued');
On free hosting with no real cron, trigger a worker URL from an external scheduler — see the cron expression builder for the scheduling side.
4. Process large jobs in chunks
If you cannot use a queue, break the work up so each request finishes quickly:
$batch = 100;
$offset = (int) ($_GET['offset'] ?? 0);
$rows = $db->fetchAll("SELECT * FROM import LIMIT ? OFFSET ?", [$batch, $offset]);
foreach ($rows as $row) { process($row); }
if (count($rows) === $batch) {
header('Location: ?offset=' . ($offset + $batch));
}
Each request handles 100 rows and redirects. Slower overall, but it never times out.
On CWP specifically
CWP enforces a maximum execution time per request. A script that exceeds it is stopped and you get a 504, with the script and line recorded in your control panel error log.
Because free accounts have no cron, background processing needs an external scheduler hitting a worker URL. Protect that URL with a secret token, or you have built an endpoint anyone can hammer.
When this means you've outgrown free hosting
If your work genuinely needs minutes of CPU per request, shared hosting is the wrong shape for it regardless of plan — the request/response model does not fit.
The answer is a background worker, which needs either a queue service or a VPS where you can run one. A bigger shared plan will not fix it.
Related errors
-
502
502 Bad Gateway
A server in front passed your request to a server behind it, and got back a broken response or none at all. -
508
508 Resource Limit Is Reached
Your account hit its CPU, memory, or concurrent-process ceiling, so the server refused the request instead of letting your site destabilise the machine. -
DB
Error establishing a database connection
PHP reached the point of trying to talk to MySQL and failed — because the credentials are wrong, the hostname is wrong, or the database server refused the connection. -
TIME
Maximum execution time exceeded
A PHP script ran longer than allowed and was stopped — almost always waiting on a slow query or a slow external service. -
429
429 Too Many Requests
You sent more requests than the server allows in a given period, so it's refusing you until the window resets.
Post the exact error and your account name on the community forum — staff and other users answer there, and the thread helps the next person who hits this.