I Switched My Booking App to Octane, Then Watched It Serve Yesterday’s Data to a New User

A ride-booking style app I maintain for a local transport startup started getting real traffic, enough that our regular PHP-FPM setup on a modest VPS was struggling under peak hours, drivers refreshing their availability screen every few seconds, customers checking fare estimates constantly. Response times were creeping up, and the client was asking about “that Octane thing” they’d read about on a Laravel newsletter.
I switched to Laravel Octane running on Swoole, followed a setup guide, benchmarked it locally, saw genuinely impressive numbers, then deployed it. Within a day, we had a support ticket that made my stomach drop, a customer reported seeing another customer’s previous fare estimate briefly flash on their screen before their own data loaded. That’s the classic Octane gotcha nobody warns you about clearly enough, and it’s exactly why understanding what Octane, FrankenPHP, and Swoole actually do differently matters way more than just chasing benchmark numbers.
Why Octane Exists in the First Place
Regular PHP, through PHP-FPM, boots your entire Laravel application fresh on every single request, loading configuration, registering every service provider, building the container, all of it, then throws that away and starts over completely for the next request. It’s wasteful, but it’s also genuinely safe, since nothing from one request can accidentally leak into the next.
Octane changes this by keeping your application booted in memory between requests, using an underlying high-performance runtime instead of the traditional PHP-FPM process model. Instead of rebuilding everything from scratch every time, Octane serves many requests off that same warm, already-booted application state. This is where the real performance gain comes from, and it’s a genuinely significant one, but it’s also exactly where my customer’s fare estimate leaked into a different customer’s session.
What Actually Happened With That Data Leak
My mistake was a static property I’d used in a service class to cache a computed fare estimate temporarily during a single request, a habit completely harmless under normal PHP-FPM, since that entire process and all its memory gets thrown away after each request anyway.
class FareEstimator
{
protected static $cachedEstimate;
public function estimate($pickup, $dropoff)
{
if (!self::$cachedEstimate) {
self::$cachedEstimate = $this->calculateFare($pickup, $dropoff);
}
return self::$cachedEstimate;
}
}
Under Octane, that static property doesn’t reset between requests, since the entire application, including that class’s static state, stays alive in memory across many different requests, potentially for many different users. My “cache it for this request” logic accidentally became “cache it forever until the worker restarts,” and one customer’s calculated fare briefly leaked into whoever’s request happened to hit that same worker process next.
The fix was straightforward once I understood the actual cause, stop relying on static properties or any kind of persisted state across requests, and use Laravel’s request-scoped container binding instead, or simply avoid static caching for anything request-specific entirely.
class FareEstimator
{
public function estimate($pickup, $dropoff)
{
return $this->calculateFare($pickup, $dropoff);
}
}
Simple enough fix, but the lesson genuinely matters, moving to Octane isn’t just a config change, it requires actually auditing your codebase for anything assuming a fresh application state per request, since that assumption quietly stops being true.
Swoole vs RoadRunner vs FrankenPHP, What They Actually Are
All three are underlying runtimes Octane can use to actually keep your app alive between requests, and each makes genuinely different tradeoffs.
Swoole is a PHP extension, meaning it needs to be compiled and installed as part of your PHP setup. It supports coroutines, letting you run things like multiple outgoing API calls genuinely concurrently within a single request using Octane’s Octane::concurrently() helper. Generally considered the most performance-focused option, particularly for workloads doing a lot of concurrent I/O, but it comes with a real operational cost, needing that extension properly installed and maintained on your server, and being the option most likely to have quirks around memory growth over time if workers aren’t recycled regularly.
RoadRunner is written in Go and runs as a separate binary communicating with PHP workers, rather than being a PHP extension itself. Generally considered easier to operate and more stable for typical CRUD-heavy applications that aren’t leaning heavily on coroutine-based concurrency, without needing a compiled PHP extension at all.
FrankenPHP is the newest of the three, built on top of the Caddy web server, packaged as a single binary. It’s genuinely gained a lot of popularity specifically because of how simple it makes deployment, one binary handling both your web server and your PHP application together, particularly appealing for containerized, cloud-native deployments where simplicity of the deployment pipeline matters as much as raw throughput.
Step 1: Actually Installing and Trying Each One
Getting Octane installed itself is identical regardless of which runtime you pick:
composer require laravel/octane
php artisan octane:install
During installation, Laravel asks which server you’d like to use. For Swoole, you’ll need the actual PHP extension installed separately first:
pecl install swoole
For FrankenPHP, no separate PHP extension is needed, since it ships as part of the FrankenPHP binary itself, genuinely simplifying that part of the setup:
php artisan octane:install --server=frankenphp
Running any of them locally for testing:
php artisan octane:start
Step 2: What I Actually Found Testing Our Booking App on Each
I want to be honest here, actual results genuinely depend heavily on your specific application’s workload, not just abstract benchmark numbers you’ll find online. For our booking app specifically, which does a fair amount of concurrent outbound calls (checking driver locations, calculating routes through a mapping API), Swoole’s coroutine support gave us a genuinely noticeable edge, since we could fire off several of these external calls concurrently within a single request using Octane::concurrently(), rather than waiting for each one sequentially.
[$driverLocation, $routeEstimate] = Octane::concurrently([
fn () => $this->mapsService->getDriverLocation($driverId),
fn () => $this->mapsService->calculateRoute($pickup, $dropoff),
]);
For a different, simpler client project, a basic content site with a contact form and some CRUD admin functionality, the difference between the three runtimes was genuinely negligible in practice, since that app barely does any concurrent I/O work that would actually benefit from Swoole’s specific strengths. For that project, we ended up going with FrankenPHP specifically because of how much simpler it made the deployment pipeline, one binary, easier to containerize, less operational overhead to think about ongoing.
Step 3: The Database Connection Gotcha (Another One I Learned the Hard Way)
Since Octane keeps your application alive between requests, database connections can behave unexpectedly too if you’re not careful. On a different project, I had some code using raw PDO directly for a specific reporting feature, bypassing Eloquent’s connection handling entirely.
Under regular PHP-FPM, this never caused issues since the whole process, connection included, gets discarded after each request. Under Octane, that raw connection persisted across requests, and a transaction that got left open due to an early return in one request’s error handling caused genuinely confusing behavior on a completely unrelated subsequent request hitting that same worker.
Octane does include listeners specifically designed to reset things like database connections and transactions between requests, but this mostly covers Eloquent’s own connection handling automatically. Custom code using raw PDO connections directly needs its own explicit cleanup, which I hadn’t originally accounted for.
// Something worth adding explicitly for custom connection handling
Octane::listen(RequestReceived::class, function () {
DB::rollBack(); // ensures no stray open transaction carries over, just in case
});
Step 4: Worker Memory Growth and Recycling
Something I didn’t fully appreciate initially, worker processes under Octane can genuinely accumulate memory over time, especially under Swoole, since PHP wasn’t originally designed with this long-running process model in mind the way some other languages are. Left unmanaged, worker memory usage can creep upward until something eventually breaks or performance degrades.
The practical fix is configuring workers to automatically restart after handling a certain number of requests, which resets their memory state cleanly:
// config/octane.php
'max_requests' => 250,
For our Swoole setup specifically, I set this fairly aggressively, around 250 requests per worker before an automatic recycle, based on observing memory growth patterns during actual load testing. For a RoadRunner-based project I worked on separately, a considerably higher threshold, closer to 500, worked fine without the same memory pressure, since RoadRunner’s process model handles this somewhat differently.
Step 5: Debugging Gets Genuinely Harder
Here’s something worth knowing before switching, Xdebug, my usual go-to for step-through debugging, doesn’t play nicely with Swoole’s process model at all. For actual production profiling and performance investigation on Octane-powered apps, I moved to using dedicated profiling tools built for this kind of long-running process model instead of relying on Xdebug the way I would for a typical PHP-FPM setup, since Xdebug’s assumptions about request lifecycle simply don’t hold the same way anymore.
Step 6: What I’d Actually Recommend Based on Workload
For a genuinely I/O-heavy application making several concurrent external calls per request, similar to our booking app’s driver location and route calculations, Swoole’s coroutine support is genuinely worth the extra operational complexity of managing that PHP extension.
For a fairly typical monolithic Laravel app handling a mix of regular web pages and some API endpoints, without heavy concurrent I/O demands, RoadRunner offers a genuinely simpler, more predictable operating experience while still getting most of Octane’s core performance benefit.
For anyone deploying into containerized, cloud-native environments where deployment simplicity and a minimal operational footprint matter as much as raw throughput, FrankenPHP’s single-binary approach is genuinely the easiest starting point, and it’s matured considerably since its initial release.
Common Mistakes I’d Warn You About
Assuming Octane is a drop-in performance upgrade with no code changes needed. As my fare estimate leak taught me directly, any assumption of a fresh application state per request needs auditing before switching.
Using static properties or singleton-style caching for anything request-specific. This is the single most common source of the “why is old data showing up” bugs people hit after adopting Octane.
Not configuring worker recycling and just hoping memory stays stable indefinitely. Set max_requests deliberately based on your own load testing, not just a default value copied from a tutorial.
Forgetting custom raw database connections need their own cleanup between requests. Octane’s automatic connection resetting mostly covers Eloquent’s own connections, not necessarily anything custom you’ve built separately.
Picking a runtime based purely on benchmark numbers without considering your actual workload. Our booking app genuinely benefited from Swoole’s coroutines specifically because of how much concurrent external API work it does. A simpler CRUD app might see barely any practical difference between the three.
Final Thoughts
That fare estimate leaking between customers was a genuinely uncomfortable bug to discover in production, but it taught me the most important thing about Octane that benchmark charts alone never really communicate, the performance gain is real and often substantial, but it comes from a genuinely different execution model than PHP developers are typically used to, and that difference has real implications for how you write code, not just how fast it runs.
If you’re considering Octane, benchmark it against your own actual application and workload rather than trusting generic numbers alone, audit your codebase specifically for anything assuming fresh state per request, and pick the underlying runtime, Swoole, RoadRunner, or FrankenPHP, based on what your app genuinely needs operationally, not just whichever one wins a synthetic benchmark you found online.
