What makes Octane monitoring different
In PHP-FPM, every request starts in a fresh process. In Octane, a worker boots once and handles thousands of requests. Three implications:
- Memory accumulates. Leaks that were invisible in PHP-FPM (the process died) now grow until max_requests recycles the worker — or it crashes.
- Singletons carry state. A service bound as a singleton keeps its properties across every request the worker handles. Good for perf, dangerous if you stash per-request data there.
- Worker crashes cascade. A fatal error in a request kills the worker, affecting every in-flight request on that worker. Octane restarts it, but the blast radius is worker-sized, not request-sized.
Detecting memory leaks
Record memory at request start AND end, tagged by worker PID:
app/Http/Middleware/OctaneMemoryProbe.php
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class OctaneMemoryProbe
{
public function handle(Request $request, Closure $next)
{
$startMb = memory_get_usage(true) / 1024 / 1024;
$response = $next($request);
$endMb = memory_get_usage(true) / 1024 / 1024;
Log::channel('octane')->info('Request memory', [
'pid' => getmypid(),
'route' => $request->route()?->uri(),
'start_mb' => round($startMb, 2),
'end_mb' => round($endMb, 2),
'delta_mb' => round($endMb - $startMb, 2),
]);
return $response;
}
}Filter by PID. A healthy worker's delta hovers around zero; a leaky worker's end-of-request memory creeps upward over time. When you spot a leaking worker, the most recent requests before the growth spike usually hold the culprit.
Octane-safe patterns
Don't stash per-request data in singletons
// Bad — currentUser persists across requests on the same worker
$this->app->singleton(UserContext::class, function ($app) {
return new UserContext($app['auth']->user());
});
// Good — bind fresh per request
$this->app->bind(UserContext::class, function ($app) {
return new UserContext($app['auth']->user());
});Use listeners for per-request reset
// app/Providers/OctaneServiceProvider.php
use Laravel\Octane\Events\RequestReceived;
use Laravel\Octane\Events\RequestTerminated;
Event::listen(function (RequestTerminated $event) {
// Clear anything that shouldn't carry between requests
app(MyStatefulThing::class)->reset();
});Driver-specific notes
FrankenPHP
Single binary. Built-in HTTPS. The simplest operational story. Worker mode shares most Octane semantics. Supports HTTP/3 which matters for latency-sensitive APIs.
Swoole
Most performant under high concurrency. Adds coroutine support — Concurrently::run([...]) fans out I/O-bound work. Requires the swoole PHP extension; more complex operational story than FrankenPHP.
RoadRunner
Go-based worker manager. Stable, mature. Less performant than Swoole at very high concurrency but easier to debug. Laravel Vapor uses it under the hood.
What to alert on
- Worker crashes — alert on crash rate > 0 sustained. Crashes usually mean Octane-incompatible code.
- Memory growth — per-worker delta trending upward over 100+ requests.
- Request latency regression — if p95 on Octane looks like PHP-FPM, something's wrong with your deploy.
- max_requests restart rate spike — if workers recycle far more often than max_requests suggests, something is OOM-killing them.
THE EASY WAY
NightOwl runs on the same laravel/nightwatch package Octane apps already use
Octane support comes from the underlying laravel/nightwatch instrumentation, which is designed to work with FrankenPHP / Swoole / RoadRunner persistent workers. NightOwl consumes that data and shows per-request memory peak (peak_memory_usage), duration, exceptions, and queries — sort the Requests page by memory to spot leaky workers as they grow.
composer require nightowl/agent
php artisan nightowl:installFrom $5/month flat. Data in your PostgreSQL.