Laravel Octane: What Changes When Your App Stays in Memory
Under PHP-FPM, every request rebuilds the world. The entry script boots the container, runs your service providers, builds the kernel, handles the request, tears it all down and throws it away. With opcache the compiled bytecode sticks around, but object construction, config merging, and provider registration still run on every hit. On a mid-size Laravel app that overhead sits somewhere between 30 and 80 milliseconds, before your own code does any work.
Octane changes the lifecycle. The application boots once when a worker starts, then the worker loops over requests with the warmed container in memory. The driver can be Swoole, RoadRunner, or FrankenPHP; the differences between them matter less than the shared idea: request handling starts from a warm state instead of a cold one.
Where the speedup actually lands
Removing boot time helps small workloads most, which is counterintuitive. An API endpoint that does 8ms of real work after 50ms of framework boot looks like a sevenfold improvement under Octane. A page that assembles a heavy report barely notices, because its own work dwarfs the boot cost you removed.
The second win has nothing to do with boot time. FPM dedicates one worker to one connection. An SSE stream under FPM holds a worker for its whole lifetime, so a thousand open dashboards hold a thousand workers, and no amount of pool tuning fixes that gracefully. An Octane worker can keep many connections alive concurrently. That is the reason we pointed long-lived event streams at Octane in our article on Server-Sent Events rather than at a bigger FPM pool.
The trade-offs, stated plainly
Octane gives up FPM's throwaway model, and that model was quietly protecting you from a whole category of bugs.
- Static state survives between requests. A service registered as a singleton that stores the current user's locale, tenant, or permissions on itself now carries that into the next user's request. The fix is a habit, not a patch: resolve request data at call time (
request(),auth()->user()) instead of accepting it in a constructor of a long-lived service. - Set globals get you. Anything set within a request (
Carbon::setTestNow, app locale, timezone overrides, mock fakes) is still set in the next request. Octane resets some known framework state between requests, but code you wrote yourself owns its own cleanup. We have seen a test-onlysetTestNowleak into a deployed build and pin every produced timestamp to 2024; under FPM that bug would have been impossible. - Memory pressure becomes real. A long-running PHP process that leaks will eventually exhaust its worker. FPM kills this class of problem daily and nobody notices. Under Octane you configure worker restart thresholds deliberately and watch memory metrics like you always had to with daemons.
- Deploys need a reload. Code is already loaded inside running workers, so swapping files does nothing until you run
php artisan octane:reload. It must be part of the deploy script, and forgetting it produces the most confusing bug reports of the Octane era: "the fix does not appear to be deployed" whilegit logsays otherwise.
When plain FPM is still the right call
Most Laravel apps answer a few requests per second, with occasional peaks. FPM with opcache handles that comfortably. If framework boot is 3% of a 400ms page render, no user will ever feel Octane, and you would pay for the privilege of tracking memory graphs and request state leaks for nothing.
Our rule: profile before you switch. Boot time is easy to measure, either from a timing header around the framework bootstrap or by comparing logs of total request duration against duration spent in application code. Switch to Octane when boot is a meaningful fraction of latency on hot endpoints, or when you need many concurrent long-lived connections. It is a configuration change with operational side effects, not an architecture migration, but the debugging habits behind it are different enough that you should do it when the numbers say so, not before.