Skip to content
Runtime8 minstable

Hardening Laravel Octane against state leaks

Moving to a persistent worker means the container survives between requests. This is the checklist that stops one request's tenant, auth, or config from bleeding into the next.

When to use this

  • You're moving a Laravel app from php-fpm to Octane (Swoole or RoadRunner), where the framework boots once and the container persists across many requests.
  • You're seeing data bleed between requests, stale configuration, or bugs that disappear after a worker restart.

Prerequisites

  • Octane installed and running locally with more than one worker.
  • A concurrency/load test harness that can interleave requests from different tenants or users.
  • An inventory of your singleton bindings and static state to audit.

Procedure

  1. 1

    Audit for request-scoped state held beyond the request

    Under php-fpm every request is a fresh process, so a singleton holding the current user or tenant is harmless — it dies at the end of the request. Under Octane that singleton lives for thousands of requests. Find every place request-scoped data is stashed somewhere long-lived.

    Surface the usual offenders
    # Singletons, static state, and anything caching a request/user/tenant.
    grep -rn "singleton(" app/ config/
    grep -rn "static \$" app/
    grep -rn "app()->instance(" app/
  2. 2

    Bind request-scoped services as scoped, and flush between requests

    Services that carry per-request state must be registered with `scoped()` (not `singleton()`) so Octane rebuilds them each request, and their state must be flushed on request boundaries. Octane's config lists services to flush between requests — add yours.

    Scoped binding + Octane flush
    // A service that carries the current tenant is scoped, not singleton.
    $this->app->scoped(TenantContext::class);
    
    // config/octane.php — reset these on every request boundary.
    'flush' => [
        TenantContext::class,
        // ...any service that must not survive a request
    ],
  3. 3

    Never hold the request, auth, or tenant in a long-lived binding

    A singleton that captures `request()`, `Auth::user()`, or the tenant at construction freezes the first request's values for the life of the worker. Resolve these live, per call, from the current context — never cache them on a long-lived object.

    Resolve live; don't freeze at construction
    // WRONG under Octane — captures request #1's user forever.
    public function __construct() { $this->user = Auth::user(); }
    
    // RIGHT — resolve at call time, every time.
    public function currentUser(): ?User { return Auth::user(); }
  4. 4

    Reset third-party and static state that assumes a fresh process

    Libraries that keep static caches, and your own static properties, assume they start empty each request. Under Octane they don't. Reset them on `RequestReceived`, or move them off static storage entirely.

  5. 5

    Treat config as immutable after boot

    `config()->set(...)` at runtime mutates shared state that the next request inherits. Configuration is read once at boot and never written at request time. If a value must vary per request, it belongs in the request-scoped context, not in config.

Verify it worked

  • Interleaved-concurrency test: fire requests for two different tenants at once, repeatedly, and assert neither ever sees the other's data.
  • Restart-independence: behaviour on request 10,000 is identical to request 1 — no drift as the worker ages.
  • Worker RSS is stable over a long run; no unbounded growth that signals accumulating per-request state.

If it goes wrong — rollback

  • Set Octane to a single worker as an immediate stopgap — it serialises requests and hides cross-request bleed while you fix the root cause.
  • If leaks are widespread, revert the route/app to php-fpm and re-introduce Octane once the audit is complete.

Want this run for you?

We implement and operate procedures like this in production systems where a misstep is expensive. Fixed scope, fixed price, defined delivery date.

Request a Fixed-Scope Architecture Blueprint