Skip to main content

Laravel Octane

Octane keeps the Laravel application resident in memory between requests using Swoole as the application server. For a Filament application with ~96 models and 3 panels, this eliminates the per-request bootstrap cost.


Why Octane for This System

OperationPHP-FPMOctane
Laravel bootstrap~50ms per requestOnce at startup
Filament panel load~40ms per requestAmortised
Permission tree loadDB query per requestCached in Octane table
Rate plan resolutionDB query per entryCached in memory

Setup

composer require laravel/octane
php artisan octane:install # select Swoole
php artisan octane:start --workers=4 --task-workers=2

Production with Nginx proxying to Octane on port 8000:

location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

The Critical Rule: scoped() vs singleton()

This is the most important Octane concept for this codebase.

singleton() — created once, shared across ALL requests forever. Use only for truly stateless services with no per-request state.

scoped() — created once per request, destroyed after the response. Use for anything that touches the current user, tenant, or request.

// AppServiceProvider.php

// SAFE — stateless, no user/tenant context
$this->app->singleton(RateResolutionService::class);

// SAFE — stateless algorithm
$this->app->singleton(InvoiceGeneratorService::class);

// MUST be scoped — holds current tenant
$this->app->scoped(CurrentTenant::class, fn() => new CurrentTenant());

// MUST be scoped — resolves current user's permissions
$this->app->scoped(RbacCheckerService::class, fn() => new RbacCheckerService());

Octane Tables (Shared Memory Cache)

Octane tables are shared memory segments accessible across all workers. Use them to cache data that is expensive to load but rarely changes — rate plans and permission trees are ideal candidates.

// config/octane.php
'tables' => [
'rate_plans' => ['rows' => 1000, 'columns' => ['data:string']],
'permissions' => ['rows' => 500, 'columns' => ['data:string']],
],
// Caching a rate plan in the Octane table
Octane::table('rate_plans')->set("firm:{$firmId}:lawyer:{$userId}", [
'data' => json_encode($ratePlan),
]);

// Reading from it — O(1), no DB query
$cached = Octane::table('rate_plans')->get("firm:{$firmId}:lawyer:{$userId}");

Warm-Up Configuration

Octane can pre-load classes before the first request hits. Add heavyweight classes to the warm array so they are ready before traffic arrives:

// config/octane.php
'warm' => [
...Octane::defaultServicesToWarm(),
\App\Models\Firm\LawFirm::class,
\App\Services\RBAC\RbacCheckerService::class,
\App\Services\Billing\RateResolutionService::class,
],

Local Development

# Hot reload on file changes (development only)
php artisan octane:start --watch

# Requires chokidar:
npm install --global chokidar-cli

Common Gotchas

Static properties persist across requests:

// DANGER
class SomeService {
private static array $cache = []; // leaks across requests
}

// SAFE — use Octane table or Redis instead

Database connections are reused: Octane reuses PostgreSQL connections across requests. If a transaction is left open after a failed request, the next request inherits the broken connection state. Always use DB::transaction() with a closure — never manually call DB::beginTransaction() without a guaranteed commit() or rollBack().

The $_SERVER and $_GET superglobals are not reset automatically: Always use Laravel's request() helper or inject Request — never access PHP superglobals directly in Octane.