In Octane, specifically Laravel Octane, understanding the behavior of singletons and distinguishing those that persist versus those that do not is crucial for leveraging its performance benefits while avoiding pitfalls like memory leaks or data leaks. Laravel Octane modifies the traditional PHP lifecycle by introducing a stateful, persistent server environment where objects can live beyond a single request, which is fundamentally different from the usual stateless PHP applications where everything resets after each request.
Singleton Persistence in Octane
A singleton in the typical Laravel context is an object that is resolved once and then kept in the application's service container for reuse. This lifetime traditionally lasts for the duration of a single web request. However, due to Octane's worker-based architecture, this lifecycle can span multiple requests, making proper management critical.
In Octane, only singletons that are resolved during the application bootstrapping (the start of the worker's life) persist across requests. This means if a singleton is resolved or created as part of the service provider's boot or register methods (the application's boot phase), that instance will be reused between all requests handled by that worker. This permanent lifecycle until the worker restarts offers performance gains by avoiding repeated instance creations but requires that these singleton instances do not hold request-specific or mutable state that might cause bugs or data leakage between users.
On the other hand, singletons resolved while handling individual requests do not persist. These singletons get constructed every request and discarded afterward. This happens because Octane uses a sandbox container per request that isolates request-specific dependencies. Once the request is finished, this sandbox container and its singletons are destroyed, preventing any accidental sharing of user-specific or transient data.
How to Identify Persistent vs Non-persistent Singletons
- Persistent Singletons:**
- Registered and resolved during application bootstrapping.
- Resolutions happen in service providers' `register` or `boot` methods.
- Explicitly "warmed" up by listing in Octane's `warm` configuration array to ensure they're resolved before handling requests.
- Retain state throughout the lifetime of the Octane worker until it is gracefully restarted or terminated.
- Suitable for state-independent services or services managing shared immutable resources (e.g., logging services, configuration repositories, caching clients that don't hold request state).
- Non-persistent Singletons:**
- Resolved during the lifecycle of a specific request.
- Created each time a new HTTP request comes in.
- Stored in a sandbox container that is cleared after the request.
- They do not share state across requests.
- Safe for request-specific or mutable state objects (e.g., request or user session aware services).
Configuring Persistent Singletons in Octane
Octane provides explicit configuration options to manage which singletons persist or get flushed between requests:
- Warm Array: The `warm` configuration option in the Octane configuration file lets developers explicitly list service classes that should always be resolved during bootstrapping. These services become persistent singletons available throughout the worker's lifecycle.
- Flush Array: This configuration option lists singletons that should be flushed (destroyed and recreated) after handling each request, even if registered as singletons. This is useful if a package or service registers a singleton but that singleton contains state that must be reset on every request.
Common Pitfalls with Singletons in Octane
Using Octane introduces a shift into a stateful paradigm from the traditional stateless PHP execution. This shift requires understanding the impact on singleton services:
- Static Properties and Data Leak: Static properties and class-level attributes persist between requests in the same worker, potentially leaking data from one user's request to another. Persistent singletons with stateful data can cause similar leaks.
- Injecting Request-Specific Objects in Singletons: A classic mistake is injecting request-scoped services like the Request or Auth objects into the constructor of a singleton. Since the singleton lives beyond the request lifecycle, it retains the initial request's data, causing incorrect behavior in subsequent requests.
- Container Injection Issues: Injecting the entire container or application instance during singleton construction can cause the singleton to hold stale service references, leading to unexpected behavior and bugs.
Best Practices to Avoid Issues
- Defer Resolution of Request-Specific Dependencies: Instead of passing request objects directly to singletons on creation, pass callbacks or retrieve them within methods. This ensures the data is fresh per method call rather than frozen at singleton instantiation.
- Scoped or Non-singleton Services: For services that must handle request-specific state, avoid singletons and opt for scoped bindings or factories that create new instances per request.
- Use of Helpers: Use Laravel helpers like `request()` or `config()` inside singleton methods to fetch the current request or configuration dynamically rather than injecting at construction.
- Managing Worker Lifecycle: Use Octane's `--max-requests` flag to restart workers after a specific number of requests, minimizing the risk of memory bloat or data corruption caused by long-lived state in persistent singletons.
- Register Singletons Wisely: Only register as singletons those services that are truly stateless or hold shared immutable data.
Technical Explanation of Octane's Container Behavior
Octane modifies the Laravel application container behavior by introducing two container pools:
1. A main container, which loads at worker boot and persists across requests.
2. A sandbox container, cloned afresh for each request to isolate request-specific bindings.
Singletons resolved from the main container during boot are persistent. Singletons resolved in the sandbox container during request handling are temporary and discarded after the request. This setup ensures efficient memory use while supporting performance gains due to minimized bootstrapping.
Examples
Typical singleton registration that creates a persistent singleton:
php
$this->app->singleton(Service::class, function ($app) {
return new Service();
});
If this registration happens in a service provider's `register` or `boot` method, the `Service` instance will persist across requests.
Singleton receiving request inside a method (for safe singleton):
php
$this->app->singleton(Service::class, function () {
return new Service(fn() => request());
});
Here, the service class might receive the request closure and call it when needed, guaranteeing fresh request data on each method call.
Summary
- Singleton persistence depends on *when* and *how* they are resolved.
- Persistent singletons are resolved at app boot, shared between requests handled by the same worker.
- Non-persistent singletons resolved during requests live for that request alone and are discarded.
- Avoid injecting request-scoped dependencies into persistent singletons.
- Use Octane configurations (`warm` and `flush`) to control singleton persistence.
- Understand and manage the stateful environment Octane creates to prevent bugs.
- Employ closures and dynamic getters for request-specific data in persistent singletons.
- Use worker restart strategies to mitigate long-term memory issues.