Laravel Octane promises a faster Laravel: keep your app booted in memory, skip the framework bootstrap on every request, do less work per request. We run a fleet of uptime-checker boxes at Oh Dear that do nothing but fire HTTP, TCP, and ping checks at the sites we monitor, so I figured they’d be a good, low-risk place to try it. We moved a handful of them from PHP-FPM to Octane on FrankenPHP , measured it properly, and then moved every one of them back.
This is the write-up of what I measured and why we reverted. Short version: for an I/O-bound workload, the gains were too small to justify the extra moving parts. Nothing’s wrong with Octane, I was just testing it on the wrong kind of workload.
The setup#
Two kinds of boxes. The quiet ones are 1 vCPU and about a gig of RAM, each handling somewhere between half a request and a couple of requests per second. The busy ones are 2 vCPU with 2 to 4 GB, doing 20-odd requests a second. Same app on all of them.
One detail drives everything here: every request makes one outbound network call (the actual check against your site) and that call is almost the entire request. A check takes 360 to 470ms end to end, and the PHP that wraps it is a rounding error next to the wait. Keep that in mind, it explains everything that follows.
I measured real traffic, before and after, over matched windows. No synthetic benchmark.
The quiet boxes: CPU went up#
Octane keeps your PHP workers warm in memory, permanently. And that’s exactly the point: no bootstrapping the framework on every request. But warm workers cost something even when idle, and the CPU you save is per-request, the bootstrap you skip each time one comes in.
At a couple of requests per second there aren’t enough requests for that saving to add up. The constant cost of staying warm wins, and total CPU ticks up a little. Tiny in absolute terms, these boxes sit around 95% idle either way, but the direction was up, not down. PHP-FPM does the opposite: it spins workers up on demand and lets them go idle, and on a quiet box, idle is most of the time.
The one thing that did improve: a trivial route dropped from around 15ms to 11ms, because the framework was already booted. A quarter faster on the part we control. But “the part we control” is single-digit milliseconds against a 400ms outbound check, so end to end nobody would ever notice.
The busy boxes#
When I first wrote about the quiet boxes I assumed the busy ones would tell a different story, that with enough traffic the skipped bootstraps would finally add up. So I cut over our two busiest checkers and left them on Octane for twelve hours of real production traffic. Here’s the whole-box CPU, php-fpm versus Octane, same boxes, matched windows:
| Box | PHP-FPM (avg / peak) | Octane (avg / peak) |
|---|---|---|
| frankfurt-1 (2 vCPU, 2 GB) | 21% / 49% | 22% / 28% |
| paris-1 (2 vCPU, 4 GB) | 25% / 33% | 22% / 26% |
Average CPU is flat. The only thing that clearly moved is the peak: frankfurt dropped from 49% to 28%, paris from 33% to 26%. Warm workers cut the per-request bootstrap spikes, so CPU is lower and smoother under load, even though the steady-state average barely budged.
Everything else was a non-event. I half expected the checks to clear faster, that with warm workers we’d chew through the queue quicker. They didn’t, because the time isn’t spent in our PHP: it’s spent waiting on the network I/O of the site we’re checking, and warm workers do nothing for that. Each box served over a million requests in those twelve hours with zero restarts, zero errors, and the outbound-bound request time stayed where it was (434ms on frankfurt, 360ms on paris). Memory went the wrong way: Octane settled around 600-670 MB of resident workers, more than php-fpm used, held in check only by recycling each worker every 1500 requests.
So the entire payoff, even on our busiest boxes, was lower CPU peaks. That’s real. It’s just not much.
Why the gains were small (it’s not a misconfig)#
I went looking to make sure I hadn’t set something up wrong. I hadn’t. What we saw on our servers is just how Octane works by design.
Octane’s worker mode has exactly one trick: boot the application once instead of on every request . That’s where the speed comes from. Everything else, your controllers, your queries, your outbound HTTP call, runs exactly the same as it does under PHP-FPM. Worker mode doesn’t make your code faster, it just stops paying the framework-boot tax per request.
For our workload that tax is tiny next to a 400ms outbound check, so there’s almost nothing to win. The benchmarks you see online that show FrankenPHP pulling 3x or 10x ahead are real, but look at what they’re benchmarking: hello-world scripts, controller-plus-view routes with no database, no I/O. CPU-bound work where framework boot is a big slice of each request. That’s exactly the workload where skipping the boot is huge, and exactly the opposite of ours.
The strongest evidence is what happens with worker mode off. In classic (non-worker) mode, FrankenPHP benchmarks within about 1% of PHP-FPM (7,023 vs 6,934 req/s in Tideways’ test). The runtime itself isn’t faster. The whole gain is the bootstrap skip, and the bootstrap skip only matters when bootstrap is a meaningful fraction of the request. For us it isn’t.
I also checked whether there was a concurrency win I was leaving on the table: could the outbound checks run in parallel? Two things killed that idea. FrankenPHP doesn’t have Octane’s concurrent-task feature
, that’s Swoole-only. And the way you’d actually parallelise outbound calls in Laravel, Http::pool(), works just fine under PHP-FPM too. It’s not a reason to run Octane. (It also doesn’t help us: each of our checks is a single outbound call, so there’s nothing to parallelise.)
The complexity tradeoff#
Against that small, real win sits the other side of the ledger. Octane on FrankenPHP isn’t something Forge manages for you, so we were running a hand-rolled serving stack: a custom systemd unit, a pinned FrankenPHP binary to keep patched, and nginx surgery to point the site at the worker instead of php-fpm. Each of those is a thing that can drift and a thing that can break, and during the rollout a couple of them did. PHP-FPM, by contrast, is what Forge gives you for free and it was already doing the job.
That’s the trade in one sentence: a parallel serving stack and a new set of failure modes, in exchange for lower CPU peaks on a couple of boxes that weren’t CPU-constrained in the first place.
When Octane is worth it, and when it isn’t#
For us it isn’t, and we’ve moved the whole fleet back to PHP-FPM.
If your requests are dominated by I/O, waiting on a database, an API, an outbound call, don’t expect Octane to move your numbers much. The bootstrap it removes is a small fraction of an I/O-bound request, so there’s little to amortise no matter how much traffic you have. We confirmed that on quiet boxes and on busy ones.
Octane earns its keep on the opposite workload: CPU-bound, framework-heavy requests, served at high rates, where booting Laravel on every request is genuinely a big cost. A dashboard, an API doing real work in PHP, anything where the time is spent in your code rather than waiting on someone else’s. There the skipped bootstraps add up to something you can measure, and the operational cost is the same whether it buys you a lot or a little.
Eventually, it all boils down to this: measure your own workload, before and after, on your real traffic. Not the hello-world number in the announcement post, and not mine. Ours said PHP-FPM, so PHP-FPM it is.