Skip to content

Running Capell on Laravel Octane

Capell supports Laravel Octane (Swoole, RoadRunner, or FrankenPHP). Octane boots the application once and reuses the same PHP process for many requests, which is where the speed comes from — and also the risk. Anything a request leaves behind in memory is still there for the next visitor, who may be a different user on a different site.

Capell ships an explicit mechanism for this. You need to understand it if you write extensions; you mostly do not need to think about it if you only host.

Capell\Core\Octane\Resettable is a one-method interface:

interface Resettable
{
public const string TAG = 'octane.resettable';
public function flushOctaneState(): void;
}

A service opts in by implementing the interface and being tagged with Resettable::TAG in a service provider:

$this->app->singleton(MyRegistry::class);
$this->app->tag([MyRegistry::class], Resettable::TAG);

Capell\Core\Octane\FlushResettableState listens for Octane’s OperationTerminated event and calls flushOctaneState() on every tagged service, resolving them from the request’s sandbox container rather than the root container so per-request overrides are respected. Registration lives in CapellServiceProvider::registerOctaneStateReset().

The listener is guarded by interface_exists(OperationTerminated::class). Without Octane installed the whole mechanism silently does nothing, so there is no cost to running Capell under PHP-FPM.

Services tagged today: CapellCoreManager, ComponentRegistry, LockdownStore, and the frontend’s ThemeViewRegistrar.

What you must do when writing an extension

Section titled “What you must do when writing an extension”

Prefer scoped over singleton for anything request-shaped. Laravel resets scoped bindings between Octane requests for you, which is why Capell binds FrontendState, FrontendContextReader, ThemePreviewContext, the page caches, and similar services that way. If your service holds the current site, page, theme, locale, user, or anything derived from the request, scoped is the answer and you are done.

DatabasePlatformRegistry and DatabaseBackupDriverRegistry are populated scoped bindings. Each operation rebuilds their adapters so cached server capabilities and configuration do not survive into the next operation. Laravel can reconnect by replacing the PDO on an existing Connection, so retaining an adapter that caches capabilities by connection would retain the old server’s capabilities. Their constructors still require explicit collections that register at least one driver; missing bindings and empty driver sets fail immediately.

Register additional platforms with DatabasePlatform::TAG during provider registration or boot, using transient or scoped adapter bindings. Tags persist when Octane resets the registry, including tags added after an earlier provider resolved it. For direct registry additions, register a container resolving callback so each new registry receives the driver; calling register() once on a resolved registry only affects that operation.

Reach for Resettable only when a service genuinely must be a singleton — a registry built once at boot — but accumulates request state alongside its boot state. In that case flushOctaneState() should clear the request state and leave the boot-time registrations intact. Clearing registrations would mean losing them for every subsequent request in that worker, because nothing re-runs boot.

Do not tag a service that is expensive to construct: the flush resolves each tagged service on every request termination.

Two rules that catch most bugs:

  • Never mutate a static property during a request unless you restore it afterwards. Static state is per-process, so under Octane it outlives the request that set it.
  • Never memoize anything derived from the locale, the site, or the config in a plain static, because the first request in a worker’s life will fix that value for all the others.

The Core hosting review identified these Octane-sensitive areas:

IssueEffect under Octane
ImageSourceType::options() memoizes translated option labels per localeFilament select labels remain correct when a worker serves requests in different locales
RenderHtmlContentAction builds its sanitizer from config for each invocationSite-scoped attribute allowlists cannot bleed between requests in a long-lived worker

Extensions that introduce similar static memoization should include every request-varying input in the memo key or use a resettable service.

  • Octane does not change any of Capell’s other hosting requirements. Multi-node caveats in the web server configuration guide apply exactly as they do under PHP-FPM.
  • Run php artisan capell:runtime-refresh as part of your deploy, then restart Octane. Refreshing caches without restarting leaves workers holding the previous boot state.
  • Marketplace package operations invalidate opcode state after Composer and record a runtime-refresh event in Package Operations. On a single node with Octane configured, Capell invokes capell:runtime-refresh, but the operator must still restart Octane so every worker boots the new providers and autoloader. On a declared multi-node host, Capell does not pretend a refresh on one node reached the others: run the command and restart Octane on every node.
  • CAPELL_MULTI_NODE is a topology declaration, not auto-detection. Set it accurately on every node. The default false can only justify single-node wording; it is not evidence that the deployment has one node.
  • When diagnosing a “wrong content for the wrong user” report, restart Octane first. If the problem disappears and then returns after some requests, it is state bleed, and the two rules above are where to look.