The first time you run php artisan octane:start and watch the same Laravel app handle ten times the throughput on the same hardware, it feels like cheating. The benchmarks pop, the dashboards calm down, and the temptation is to ship it that afternoon. Don't.
Octane gives you real performance, but it does it by changing the rules of how a Laravel request actually runs. PHP-FPM boots the framework, handles one request, and throws everything away. Octane boots the framework once and reuses the same worker process across thousands of requests. That single change is the source of every performance win and every weird production bug you're about to meet.
What Octane Actually Does
Under PHP-FPM, the lifecycle is "boot, serve, die." Every request reloads the autoloader, re-resolves the container, re-reads config, re-instantiates service providers. That's the slowest 30–60ms of a typical Laravel request, and it happens every single time.
Octane runs Laravel inside a long-lived application server. There are four supported runtimes in 2025: Swoole and Open Swoole (PHP extensions), RoadRunner (a Go binary that talks to PHP workers), and FrankenPHP (a Caddy-based server that ships with PHP embedded — currently the most ergonomic option).
Install is two lines:
composer require laravel/octane
php artisan octane:install
The installer asks which server. Pick FrankenPHP unless you have a reason not to — it's the simplest to deploy, supports HTTP/2 and HTTP/3 out of the box, and doesn't need a separate extension. Then start it:
php artisan octane:start --server=frankenphp --workers=4 --max-requests=500
Four workers, each handling up to 500 requests before being recycled. That last flag is your safety net for memory leaks, and you'll learn to love it.
State Is The Whole Game
Here's the rule that breaks people's mental models: anything you put on a singleton, a static, or a container binding stays there until the worker dies.
A common pattern under FPM looks like this:
$this->app->singleton(Tenant::class, function () {
return Tenant::resolveFromRequest(request());
});
Under FPM that's fine — the container is fresh on every request. Under Octane, the first request that hits this worker resolves a tenant, caches it on the container, and every subsequent request on that worker gets the same tenant. Including requests from other tenants. That's not a slowdown, that's a data leak.
The fix is either to use scoped bindings (scoped() instead of singleton(), which Laravel resets between Octane requests) or to resolve request-scoped state from the request itself, never from a long-lived container binding.
// Reset every request — safe under Octane.
$this->app->scoped(Tenant::class, fn ($app) => Tenant::resolveFromRequest($app['request']));
The same logic applies to facades that hold state, models with global scopes that read from the container, and any package that "remembers" something. Audit your service providers with one question: does this object hold data that belongs to one request?
Memory Leaks Are Now Your Problem
Under FPM you could write a memory-hungry endpoint, leak a few megabytes, and never notice — the process died at the end of the request. Under Octane, that few megabytes survives. Multiply by a thousand requests and your worker is at 2 GB.
The classes of leak I see most often:
- Static caches that grow without bounds. Someone added
static $cache = []; $cache[$key] = $value;years ago. Under FPM it was a per-request memo. Under Octane it's a process-lifetime memory leak. - Logger or telemetry buffers that never flush. Some packages collect events into a buffer keyed off the request, and forget to clear it.
- Eloquent global scopes that hold collections. A static model collection is the easiest hot leak to write by accident.
- Container bindings made via closures that capture large objects. The closure holds a reference, the binding holds the closure, the worker holds the binding.
The two operational defenses are --max-requests (recycle the worker after N requests) and a memory-watching healthcheck that restarts the worker if RSS exceeds a threshold. Set both. Treat memory leaks as a real category of bug, not a "we'll get to it" item.
Things That Used To Be Free Now Cost You
A handful of habits that were harmless under FPM become hot paths under Octane.
config() lookups are still cheap, but reading a deeply nested config value 10,000 times per second is a measurable cost when nothing else has any overhead. Cache the value once. The Octane in-memory cache (Cache::store('octane')) is per-worker and lives in process memory — perfect for things like feature flags, parsed config, or anything that's stable for the worker's lifetime. Worth knowing: this cache driver is Swoole-only (it's backed by Swoole tables), so on FrankenPHP or RoadRunner you'll lean on a regular array store or just a static property instead.
OctaneServiceProvider::boot() runs once per worker, not once per request. That's where pre-warming lives. The octane.warm config array lets you list services to resolve on worker boot:
// config/octane.php
'warm' => [
\App\Services\PricingTable::class,
\App\Services\GeoIpDatabase::class,
],
Anything that's expensive to build and stable across requests should warm here. Anything that's per-request must not.
There's also a tick mechanism for periodic in-process work — useful for refreshing in-memory caches without hitting Redis on every request. Same caveat as the Octane cache: ticks are a Swoole-only feature.
Octane::tick('refresh-flags', fn () => FeatureFlags::refreshFromDb())
->seconds(30)
->immediate();
That fires every 30 seconds inside the worker. You don't need a separate cron entry for it.
Hot Reloading And The Local Loop
Local dev has one annoying difference from FPM: code changes don't show up until the worker restarts. Run Octane with --watch and it'll restart workers when files in app/, config/, routes/, and resources/ change. It's not as instant as FPM, but it's close enough that you stop noticing.
php artisan octane:start --server=frankenphp --watch
In CI, run your tests against PHP-FPM the same way you always have — Octane is a runtime concern, not a behavior change for the framework. If your tests pass under FPM, they'll pass under Octane unless you're leaning on per-request container resets, which is the bug you wanted to find anyway.
When Octane Earns Its Keep
Octane is a real performance improvement, but it's also a real operational responsibility. Decide based on what your bottleneck actually is.
You want Octane when PHP boot time is the dominant cost — high-throughput APIs, latency-sensitive endpoints, anything where you're paying 30ms of framework boot for 5ms of actual work. You want it when you have the engineering capacity to audit service providers, watch memory, and write request-aware code.
You don't want Octane when your bottleneck is the database or an external API — speeding up the framework boot doesn't help when you're waiting 800ms on a query. You don't want it on a small team that's still figuring out where their state lives. PHP-FPM 8.3 with OPcache and a sensible cache layer is fast enough for most CRUD apps and far less work to operate.
If you do ship Octane, the rules that hold up are simple: scope your bindings carefully, recycle workers, monitor memory, and treat every singleton as a thing that survives until you kill it. The performance is real. The new responsibilities are also real. Knowing which side of that trade you're on before you flip the switch is the whole job.



