# 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.

Source: https://cwp.sg/errors/504-gateway-timeout/  
Author: Ben Johnson  
Last verified: 2026-08-04

**Also seen as:** Gateway Timeout, 504 error, request timed out

## What it 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:

```sql
EXPLAIN SELECT * FROM orders WHERE customer_email = 'a@b.com';
```

If the `type` column says `ALL`, it is scanning the entire table. Add an index:

```sql
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:

```php
$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:

```php
$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](/tools/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:

```php
$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 hosting 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 have 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.

---

Content Website Platform (cwp.sg) operates the free hosting it writes about; see https://cwp.sg/about/ for the methodology and the commercial disclosure.
