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
| Operation | PHP-FPM | Octane |
|---|---|---|
| Laravel bootstrap | ~50ms per request | Once at startup |
| Filament panel load | ~40ms per request | Amortised |
| Permission tree load | DB query per request | Cached in Octane table |
| Rate plan resolution | DB query per entry | Cached 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.