# Response time: what governs it, and how to check

Written after an incident on 24 August 2026. Logins were timing out at 30
seconds on both the local and the development server, and the fatal error
pointed at Laravel's password hasher:

```
PHP Fatal error: Maximum execution time of 30 seconds exceeded in
  vendor\laravel\framework\src\Illuminate\Hashing\AbstractHasher.php on line 32
```

The hasher was innocent. bcrypt measured **230 ms** — normal for cost 12, and
the cost is a security setting that should not be lowered. The hasher was
simply where the clock happened to run out.

## The actual cause

**opcache was not enabled.** The extension shipped with PHP and the DLL was
present, but `php.ini` had it commented out:

```ini
;zend_extension=opcache
;opcache.enable=1
```

Without it PHP recompiles every file it touches on every single request. This
project loads around **9,900 PHP files** from `vendor/`. Measured on the same
machine, same database, same moment — one server with opcache, one without:

| | no opcache | opcache on |
|---|---|---|
| one list request | 594 ms | 62 ms |
| one login | 1,077 ms | 590 ms |
| a page firing 20 requests | **9,496 ms** | **1,075 ms** |

A page load went from 9.5 seconds to 1.1 — **8.8× faster**. Requests that had
been queueing behind each other for half a second each now clear in tens of
milliseconds, and the 30-second ceiling stopped being reachable.

Where the time had been going, per request:

```
composer autoload ..........  224 ms
bootstrap/app.php ..........   44 ms
framework bootstrap ........  504 ms   <-- recompiling the framework
first database connection ..  199 ms
a query, once connected ....  0.4 ms   <-- the database was never the problem
```

## The fix

### Local (Windows, PHP at `C:\php`)

Already applied. In `C:\php\php.ini`:

```ini
zend_extension=opcache
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=0
```

`validate_timestamps=1` with `revalidate_freq=0` means PHP checks whether a
file changed on each request and recompiles only that file. That is what you
want while developing: full speed, and edits still take effect immediately.

**A running server must be restarted** — PHP reads `php.ini` once at startup,
so `php artisan serve` keeps the old configuration until you stop and start it.

The previous file is backed up at
`C:\php\php.ini.backup-before-opcache-2026-08-24`.

### Development / production server (cPanel)

The same setting, through **MultiPHP INI Editor**:

1. cPanel → *Software* → **MultiPHP INI Editor**
2. Pick the domain, switch to **Editor Mode**
3. Add the block above
4. Save, then restart PHP (or wait for the pool to recycle)

On a shared host `opcache.memory_consumption` may be capped; 128 is enough for
this application.

For production also run, on each deploy:

```
php artisan optimize        # config, routes and events cached
```

Do **not** run that locally — a cached config ignores later `.env` edits, which
is a confusing half-hour.

## Checking it

```
php artisan app:check-performance
```

Reports opcache, the Laravel caches, `APP_DEBUG`, bcrypt cost and the database
handshake, and names anything that needs fixing. Run it on any environment,
and after any server move or PHP upgrade.

The failure mode this catches is a quiet one: nothing logs "opcache is off".
The application behaves perfectly and is merely twenty times slower, which
reads as "the server feels sluggish today" until it crosses a timeout.

## Two things that are not faults

**bcrypt costs ~230 ms per login.** That is the password check doing its work.
Cost 12 is the correct setting; each round below it halves an attacker's cost
too. Do not lower it to make logins feel quicker.

**The built-in server handles one request at a time.** `php artisan serve` and
`php -S` are single-threaded, so twenty requests queue: measured at 85, 155,
211, 273, 343, 416 ms for six identical requests — each waiting for the one
before. With opcache at ~60 ms per request this is unnoticeable. It is also
local-only: cPanel runs PHP-FPM or LiteSpeed, which serve requests in parallel.

If local concurrency ever does matter, use Laragon, XAMPP or `php artisan
octane` rather than the built-in server.
