Laravel Octane promises something that sounds too good: your Laravel app, running at speeds that make PHP-FPM look like it's wading through mud. Thousands of requests per second. Response times in single-digit milliseconds.
It's real. I've seen it. But between the marketing and your production server there's a gap the docs gloss over, and it's full of memory leaks, stateful landmines, and 3am restarts. Here's what I actually learned running Octane for real.
Pick your server like it matters, because it does
Octane gives you three engines: FrankenPHP, Swoole, and RoadRunner. The docs present them as roughly equal choices. They're not.
FrankenPHP is what I'd hand to most teams. It's a Caddy-based app server, installs without drama, and worker mode just works. If your team already deploys PHP the boring way, this is the smallest jump with the biggest payoff.
RoadRunner is the grown-up in the room. Written in Go, mature process model, great supervisor story. If you want boring reliability and you don't mind one more binary in your stack, it's hard to beat.
Swoole is the fastest on paper and the one I'd least like to debug at 3am. It's a PHP extension that rewrites the rules of the runtime — coroutines, its own event loop, its own table implementation. When it works it's beautiful. When it doesn't, you're reading C-level stack traces wondering where your evening went.
My take: start with FrankenPHP. Move to RoadRunner if you outgrow it or need its process model. Only reach for Swoole if you know exactly why you need it.
Your app is stateful and you forgot
This is the big one. Under PHP-FPM, everything dies at the end of every request. Your sloppy singleton? Gone. That static property accumulating data? Gone. FPM is a garbage collector with a web server attached.
Octane keeps your app booted in memory across requests. Suddenly all that state you were accidentally relying on FPM to clean up... doesn't get cleaned up.
The classic hits:
// This looks innocent. Under Octane it's a memory leak with a login form.
class ReportService
{
protected static array $rows = [];
public function addRow(array $row): void
{
static::$rows[] = $row; // grows forever, across requests, across users
}
}
Singletons bound with request-scoped data. Event listeners registered in a service provider that never get deregistered. Config values mutated at runtime (config(['app.timezone' => ...]) in one request leaking into the next). Locale or timezone set per-request and never reset.
The fix isn't complicated, but it requires a mindset shift: treat every request as sharing a room with every other request. Audit your singletons. Prefer bind() over singleton() for anything request-scoped. And use Octane's lifecycle hooks — Octane::tick(), request "flush" callbacks — to reset what must not survive.
If your codebase is old and nobody knows what the singletons hold, be honest with yourself: Octane will find every one of them, in production, on a Friday.
Memory leaks are not an if, they're a when
Even a clean app leaks a little under a long-lived worker. A few kilobytes per request doesn't matter at 100 requests. At 100,000 requests on the same worker, it matters a lot.
So don't run workers forever. Octane lets you cap it:
// config/octane.php
'max_requests' => 500, // recycle the worker after 500 requests
Pick a number and stop theorizing. 500 is a fine default. Watch worker memory in your monitoring — if it climbs steadily between recycles, you have a leak worth hunting. If it stays flat, your number is fine and you can raise it.
And set up octane:reload as part of your deploy, not as an afterthought. A deploy that doesn't recycle workers is a deploy that didn't fully happen.
Concurrency is a tool, not a lifestyle
Octane can run tasks concurrently (Octane::concurrently()), and it's genuinely useful for the fan-out pattern: three independent API calls, one response. I've replaced whole queue jobs with it where the work was fast and the user was waiting anyway.
But don't turn your request lifecycle into a concurrency puzzle. If you need real background work with retries and failure handling, that's what queues are for. Octane doesn't replace Horizon. It replaces the part of your code that was waiting on I/O for no reason.
Deploy it like a grown-up
A few things that bite people on day one:
- Supervisor (or systemd) is not optional. Workers die. OOM-killer exists. Your process manager should restart them without you noticing. Here's the shape of it:
[program:octane]
command=php /var/www/artisan octane:start --server=frankenphp --workers=auto --max-requests=500
autostart=true
autorestart=true
user=www-data
-
--workers=autois a starting point, not an answer. Start there, then size workers to your actual CPU and memory. More workers than cores just adds context-switching overhead. - OPcache + preloading still matter. Octane doesn't make PHP's compilation cost disappear for free; keep your OPcache tuned like you would anywhere else.
- Warm your caches before traffic arrives. First requests on fresh workers are the slowest. A deploy-time warmup script hitting your key routes saves you from a latency spike right when you're watching the dashboards.
When I'd skip Octane entirely
Not every app should run on it, and saying so isn't heresy:
- Low-traffic apps where FPM is already idle 99% of the time. You're adding operational complexity for numbers nobody will notice.
- Apps with heavy per-request memory (big file processing, huge collections) — long-lived workers amplify this instead of containing it.
- Codebases where nobody will fix the leaks Octane exposes. The server is honest; your app has to be too.
Octane is a performance tool for apps that have outgrown FPM's model, not a magic flag you flip on everything.
The verdict
Octane is the real deal — the performance gains aren't marketing. But it's a different runtime model wearing Laravel's clothes, and it demands you actually understand what your app holds in memory.
Do the singleton audit. Cap your worker lifetimes. Pick FrankenPHP unless you have a reason not to. Deploy with a process manager and a reload strategy from day one.
Get those right and Octane is the easiest performance win in the Laravel ecosystem. Get them wrong and you'll learn about memory leaks the hard way — which, to be fair, is also a way to learn.
Top comments (0)