0

Laravel Octane — long-lived processes, what changes

Advanced5 min read·php-11-008
laravel-srcperformance

Concept

Laravel Octane fundamentally changes PHP's execution model. Traditional PHP: each HTTP request boots the framework (loads all service providers, registers all bindings, sets up all singletons) — then processes the request — then discards everything. With Octane, the framework boots ONCE, and each subsequent request reuses the same bootstrapped application.

Workers: Octane runs your application in a long-lived PHP process using Swoole or RoadRunner. The process boots Laravel once, then loops: accept request → reset state → handle request → send response → repeat. No bootstrap overhead per request. No OPcache revalidation. Benchmarks show 2-10× throughput improvement.

What changes in your code:

  1. No per-request globals: $_GET, $_POST, $_SERVER are only valid for the current request cycle. Octane resets them but any code that caches them statically between requests will serve stale data to the next request.
  2. No static state between requests: Static class properties persist across requests. If your code sets SomeService::$state = 'something' and never resets it, the second request sees the first request's state.
  3. Singletons are true singletons: In traditional PHP, a singleton only lives for one request. In Octane, it lives for the process lifetime. Services that hold per-request state (authentication, localization) must be properly reset between requests or must NOT be singletons.
  4. Memory leaks are real: Object cycles, accumulated static state, and un-freed resources accumulate over thousands of requests. Monitor memory and restart workers periodically.

Octane service providers: Laravel Octane introduces flush bindings — services that should be re-created per request even in the long-lived process. Use $this->app->scoped() instead of $this->app->singleton() for per-request services.

Code Example

php
<?php
// Installation: composer require laravel/octane
// php artisan octane:install --server=swoole
// php artisan octane:start --workers=4

// ===== PROBLEM: static state persists across requests =====
class StatefulService
{
    private static array $cache = []; // persists across requests in Octane!

    public static function process(string $key): string
    {
        if (!isset(self::$cache[$key])) {
            self::$cache[$key] = expensiveComputation($key);
        }
        return self::$cache[$key];
    }
}
// FIX: use instance state or per-request container binding

// ===== PROBLEM: singleton holds per-request data =====
class UserContext
{
    private ?User $currentUser = null; // in Octane: persists across requests!

    public function setUser(User $u): void { $this->currentUser = $u; }
    public function getUser(): ?User { return $this->currentUser; }
}
// FIX: use scoped binding (resets per request)
// In ServiceProvider:
// $this->app->scoped(UserContext::class); // re-instantiated per Octane request

// ===== Octane-aware service provider =====
use Laravel\Octane\Facades\Octane;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // scoped() = singleton within a request, new instance per request in Octane
        $this->app->scoped(UserContext::class);
        $this->app->scoped(RequestContext::class);
    }
}

// ===== Memory monitoring in long-lived processes =====
// Octane worker restart after N requests:
// OCTANE_MAX_REQUESTS=500  in .env
// php artisan octane:start --max-requests=500

// Monitoring memory leak
Octane::tick('memory-check', function() {
    $mb = memory_get_usage(true) / 1024 / 1024;
    if ($mb > 256) {
        logger()->warning("High memory: {$mb}MB — consider restarting worker");
    }
})->seconds(60);