Why Long-Running Processes Matter
Traditional PHP runs each request in isolation: the web server spawns a process, executes your code, and tears it down. This works, but it's inefficient. You're bootstrapping Laravel, loading all service providers, and reconnecting to databases on every single request. Laravel Octane: PHP & Laravel best practices explained represents a fundamental shift—we keep the app alive across multiple requests, dramatically reducing overhead and boosting throughput.
If you've built SaaS platforms or e-commerce systems handling thousands of concurrent users, you know how much latency matters. Octane can cut response times by half or more because we're not paying the bootstrap tax repeatedly.
Understanding the Three Runtimes: Swoole, RoadRunner, and FrankenPHP
Laravel Octane doesn't reinvent the wheel—it's built on top of proven long-running server technologies. Each has different trade-offs, and choosing the right one depends on your infrastructure and workload.
Swoole
Swoole is a C extension that gives PHP async capabilities and coroutines. It's the most mature option and offers exceptional performance. When you run php artisan octane:start --server=swoole, you get a full event loop handling concurrent requests within the same process.
Strengths: Maximum throughput, WebSocket support, battle-tested in production. Weaknesses: requires the C extension compiled into your PHP binary, can be tricky to debug memory leaks, and demands careful state management.
RoadRunner
RoadRunner is a standalone application server written in Go that communicates with PHP over sockets. Your application doesn't know it's running under RoadRunner—it just receives requests and returns responses. This abstraction layer makes it safer: if your code crashes, RoadRunner restarts just that worker.
Strengths: Language-agnostic foundation, worker pool isolation, easier debugging. Weaknesses: slightly more overhead than Swoole due to the Go-PHP bridge, but still blazingly fast.
FrankenPHP
The newcomer. FrankenPHP embeds PHP directly into a Go application, offering simplicity and built-in HTTPS support. It's ideal for edge deployments or containerized environments where you want a single binary.
Strengths: Single binary distribution, built-in TLS, modern architecture. Weaknesses: younger ecosystem, fewer production deployments at scale, less community knowledge to draw from.
Dramatic Throughput Gains in Practice
Let's talk numbers. We recently migrated a CRM platform from traditional FPM to Octane with Swoole. Request latency dropped from ~200ms to ~40ms on average. Peak throughput went from 500 requests/second to 2,000 requests/second on the same hardware.
The gains come from several sources:
- No bootstrap overhead—the kernel, service providers, and dependency container are initialized once and reused.
- Persistent database connections—connection pooling eliminates the handshake tax.
- Warm opcache—PHP bytecode stays compiled in memory.
- Concurrency—multiple requests are handled in parallel within a single worker process.
Most projects see a 3-5x improvement with conservative tuning. Some high-throughput systems see 10x gains. The catch? You have to respect the long-running process model.
The Critical Problem: State Leaking Between Requests
Here's where most developers stumble. In FPM, each request is isolated—your static variables, globals, and singletons are cleaned up automatically. With Octane, they're not. A single mistake can cause data from Request A to leak into Request B.
Imagine this scenario in your authentication middleware:
class AuthMiddleware
{
private static ?User $cachedUser = null;
public function handle(Request $request)
{
if (self::$cachedUser === null) {
self::$cachedUser = auth()->user();
}
return self::$cachedUser;
}
}In FPM, this is fine—the static property dies after the request. In Octane, the first user is permanently cached. Request 2 gets the first user's data. Security disaster.
The issue is subtle because Laravel's design assumes request isolation. You're not doing anything "wrong" by PHP standards—you're just building on assumptions that no longer hold.
Resetting Singletons and Managing State
The solution is systematic: identify every component that holds request-scoped or user-scoped state, and reset it between requests. Laravel provides lifecycle hooks to do this cleanly.
Using the Octane Tick
Register handlers that fire after every request:
// bootstrap/app.php or a service provider
use Laravel\Octane\Octane;
Octane::tick(function () {
// Clear caches that shouldn't persist across requests
cache()->flush();
});
Octane::onWorkerStart(function () {
// Initialize pooled resources
Log::info('Worker started');
});
Octane::onWorkerStop(function () {
// Cleanup
Log::info('Worker stopping');
});This runs after every request completes, giving you a safe point to reset state.
Flushing Service Container Bindings
Many packages bind request-aware singletons. The auth driver, for instance, caches the authenticated user. Reset it explicitly:
Octane::tick(function () {
// Reset authentication
auth()->logout();
// Reset eloquent query builder state
DB::flushQueryLog();
// Clear request-scoped config
config()->flush();
});Guarding Singletons in Your Own Code
If you're writing services that cache things, make them request-aware:
class UserRepository
{
private array $cache = [];
public function findById(int $id): User
{
return $this->cache[$id] ?? User::find($id);
}
public function flushCache(): void
{
$this->cache = [];
}
}
// In your Octane tick:
Octane::tick(function () {
app(UserRepository::class)->flushCache();
});The principle is simple: if a service holds request or user-specific data, it must be cleared between requests.
Database Connection Pooling
One of the biggest wins with Octane is connection pooling. Configure it in your database config:
// config/database.php
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT'),
'pool' => [
'min' => 2,
'max' => 10,
],
// ... other settings
],Octane maintains a pool of persistent connections. You still get fresh logical transactions per request (Laravel's query builder handles that), but you eliminate TCP handshakes and authentication overhead.
Memory Considerations
Long-running processes accumulate memory. A small leak that's invisible over a single request becomes a crash over a million requests. Monitor this aggressively:
- Enable
memory_limittuning—start conservative (512MB per worker) and increase if needed. - Profile your app with tools like Blackfire to spot leaks.
- Consider periodic worker recycling: Octane can restart workers after N requests.
- Watch for unbounded caches or collections that grow during the lifetime of a request.
In production, we typically set worker restarts to happen every 10,000 requests or hourly, whichever comes first. This gives you most of the performance benefit while keeping memory stable.
Common Mistakes
- Assuming singletons are per-request. They're not. Every binding in the container persists unless you explicitly reset it.
- Forgetting to reset authentication context. This is the most common security bug. Always flush auth between requests.
- Storing user data in static properties or class variables. Use request-scoped services instead.
- Not testing under load. Octane problems often only appear under concurrency. Load test early.
- Ignoring memory growth. Monitor memory usage continuously. A 1MB leak per 1000 requests means trouble.
- Mixing Octane and traditional FPM code without auditing. Packages built for FPM might assume isolation—vet dependencies carefully.
Monitoring and Debugging
Run php artisan octane:status to see worker health. Use New Relic, Datadog, or similar to track memory and latency in real time. Set up alerts for memory creep.
When debugging state leaks, add request IDs to your logs and trace a transaction across multiple requests. You'll quickly see where data is leaking.
Key Takeaways
- Laravel Octane eliminates the bootstrap overhead of traditional FPM by keeping the application alive across requests, delivering 3-10x throughput improvements.
- Choose Swoole for maximum performance, RoadRunner for safety and ease of deployment, or FrankenPHP for modern simplicity.
- State leaking between requests is the primary risk—singletons, static variables, and cached user data must be explicitly reset using
Octane::tick(). - Always flush authentication context, clear caches, and reset request-scoped services between requests to prevent security and data corruption bugs.
- Connection pooling makes database performance dramatic, but monitor memory growth aggressively and consider periodic worker recycling.
- Load test your application under Octane before shipping to production—isolation bugs only surface under concurrency.






Comments
Loading comments…