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

Source: https://cwp.sg/errors/maximum-execution-time-exceeded/  
Author: Ben Johnson  
Last verified: 2026-08-04

**Also seen as:** Fatal error: Maximum execution time of 30 seconds exceeded, PHP timeout, script timeout

## What it means

PHP caps how long a single script may run. Exceed it and PHP kills the script mid-execution:

```
Fatal error: Maximum execution time of 30 seconds exceeded
in /home/u1234/htdocs/wp-content/plugins/importer/run.php on line 84
```

Unlike memory errors, **the line named here is usually accurate** — it's where the script was when the clock ran out. That's the slow operation.

One important detail: the timer counts *PHP execution* time, not wall-clock time. Time spent waiting on a database query or an HTTP request often doesn't count on some configurations, which is why a script can appear to hang for far longer than the limit before dying.

If a proxy sits in front, you may see [504 Gateway Timeout](/errors/504-gateway-timeout/) instead, because the proxy gave up before PHP did.

## Why it happens

### A database query with no index

The most common cause by a wide margin. A query scanning a large table takes seconds. Add a few of those to one page load and the limit arrives.

The tell is that nothing in the code changed — the table simply grew past the point where the missing index mattered.

### An external service that went slow

Your page calls a payment provider, a feed, a social widget. That service slows down and your page waits with it. Without an explicit timeout, PHP waits up to `default_socket_timeout` — 60 seconds by default — for each call.

### Bulk work in a web request

Imports, exports, sending hundreds of emails, resizing a media library. These take minutes by nature and don't belong in a request that a person is waiting on.

### A loop that never ends

A `while` whose condition is never met, or recursion without a base case. If this appeared immediately after a code change, start here.

## How to fix it

### 1. Time the operations to find the slow one

Don't guess. Measure:

```php
$start = microtime(true);
$result = $db->query($sql)->fetchAll();
error_log(sprintf('query took %.2fs', microtime(true) - $start));
```

On WordPress, `define('SAVEQUERIES', true);` plus Query Monitor lists every query with its duration. Anything over 200ms is worth attention; anything over a second is your problem.

### 2. Index the column you're filtering on

Once you know the slow query, check what it's doing:

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

If the `type` column says `ALL`, it's reading every row. Add an index:

```sql
CREATE INDEX idx_customer_email ON orders (customer_email);
```

This routinely turns a 30-second query into a few milliseconds. It is the single highest-value fix in this whole page.

### 3. 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,
    CURLOPT_CONNECTTIMEOUT => 2,
]);
$body = curl_exec($ch);
if ($body === false) {
    $body = get_cached_copy();   // degrade rather than hang
}
```

Then cache the response so you aren't making the call on every page load.

### 4. Break long jobs into chunks

If the work genuinely takes minutes, make each request do a slice of it:

```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));
    exit;
}
```

Slower overall, but it never times out and you can watch it progress.

### 5. Raise the limit only for the script that needs it

```php
set_time_limit(300);   // this script only, not site-wide
```

Use it for a genuine one-off — a migration, a bulk import you run manually. Do not raise it globally: a site-wide limit of 300 seconds means one stuck script holds a process slot for five minutes, and ten of those exhaust your plan.

Many shared hosts cap this above PHP, so the call may have no effect.

## On CWP hosting specifically

CWP enforces a maximum execution time per request above PHP, so `set_time_limit()` cannot raise it past the plan's ceiling.

Because free accounts have no cron, chunked processing driven by redirects — or an external scheduler hitting a worker URL — is the practical way to run long jobs. The [cron expression builder](/tools/cron-expression-builder/) covers the scheduling side.

## When this means you have outgrown free hosting

Work that legitimately needs minutes of CPU per request doesn't fit the request/response model at all, on any plan.

The correct fix is a background worker, which needs a queue service or a VPS. A bigger shared plan buys you a slightly longer timeout and the same architectural problem.

---

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