Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Laravel Octane: What It Is, When It Helps, and the State Leaks Nobody Warns You About

About Post

Classic PHP has the memory of a goldfish, and that's one of its best features.

Every request starts from nothing. Load the framework, register the service providers, read the config, handle the request, send the response, forget everything. The next request starts from nothing again. Nothing you did to memory in one request can possibly affect the next.

Laravel Octane removes the goldfish. Your app boots once and stays in memory, serving request after request. That makes it faster, and it also means a whole category of bugs that couldn't exist before suddenly can. Let's look at both sides.

What Octane actually does

With the usual setup (Nginx or Apache plus PHP-FPM), much of the work in a request is bootstrapping: loading classes, building the service container, registering providers, loading config and routes. OPcache helps a lot with the loading part, but the booting still happens on every request.

Octane runs your app on a long-lived application server, FrankenPHP, Swoole or RoadRunner. Each worker boots Laravel once, then keeps that booted application in memory and hands it request after request. The per-request boot cost mostly disappears.

Getting started is short:

composer require laravel/octane
php artisan octane:install          # choose FrankenPHP, Swoole or RoadRunner
php artisan octane:start --workers=4 --max-requests=500

That's the easy part. The interesting part is what "stays in memory" means for code that was written assuming the goldfish.

The new rule: memory survives between requests

Under PHP-FPM, singletons and static properties live for one request. Under Octane, static properties, and any singleton resolved while the application boots, live for the lifetime of the worker: potentially hundreds of requests from different users.

Octane resets the framework's own state between requests, and gives each request a fresh copy of the container to resolve things into. But it can't know what your code tucked away in the shared parts. Here are the usual leaks.

Leak 1: a singleton that captured the request

This looks perfectly reasonable in a multi-tenant app:

// AppServiceProvider::register()
$this->app->singleton(CurrentTenant::class, function ($app) {
    return new CurrentTenant($app['request']->user()?->tenant_id);
});

Under PHP-FPM, it's built fresh for every request. Under Octane, if it gets resolved while the app boots (from a service provider's boot() method, from another singleton that needs it, or because it's in Octane's warm list), it's built once per worker, with whatever request was around at that moment. Every later request on that worker shares it. Whether it happens depends on resolution order, which is exactly why it slips through testing. That isn't a performance bug. It's a data leak.

The fix is a scoped binding, which Laravel flushes at the start of every request (and every queued job):

$this->app->scoped(CurrentTenant::class, function ($app) {
    return new CurrentTenant($app['request']->user()?->tenant_id);
});

More generally: don't inject the request, the container or the config repository into the constructor of anything that's a singleton. Resolve what you need at the moment you use it.

Leak 2: the static cache that never empties

class ExchangeRates
{
    private static array $rates = [];

    public function rate(string $currency): float
    {
        return self::$rates[$currency] ??= $this->fetchRate($currency);
    }
}

Under PHP-FPM, that's a harmless per-request memo. Under Octane, the array lives as long as the worker. The rates are never refreshed, and if the key is something unbounded (a user ID, a search term), memory grows until the worker is recycled. Use the real cache with an expiry, or keep the memo on an object that's created per request.

Leak 3: setters on shared services

A service that lives for the whole worker (a boot-time singleton, or anything held in a static property) with a setCurrentUser() method, or a $warnings[] array that collects messages "for this request", or a flag like $this->skipNotifications = true set by one controller action. Octane resets a lot of the framework's own state for you, but it has no idea these properties exist. Whatever the last request left on that object, the next request on the same worker starts with.

The Octane rule: assume every object you didn't create inside the current request may have served someone else before. If it holds per-request or per-user data, it must be scoped, not shared.

Safety nets worth using

  • --max-requests recycles each worker after a number of requests, which caps the damage from slow memory leaks. It's a seatbelt, not a fix.
  • php artisan octane:reload in your deploy script. Workers hold the old code in memory, just like queue workers; without a reload, your deploy isn't live.
  • --watch in local development, so the server restarts when files change, instead of you wondering why your edit did nothing.
  • Test under Octane before production. Run the app with Octane locally or in staging and use it as two different users in two browsers. Leaks like the first one tend to show up quickly that way.
  • Check your packages. Most popular packages handle long-lived workers well now, but anything that keeps static state deserves a look.

When Octane helps

  • High-traffic APIs where bootstrapping is a real share of each request's time, such as many small, fast requests from a mobile app.
  • Apps with heavy boot costs: lots of service providers, packages and configuration.
  • When you need more throughput from the same servers and you've already done the cheaper work (queries, indexes, caching).

When it doesn't (or isn't worth it yet)

  • Your requests spend most of their time waiting on the database or external APIs. Octane removes boot time, not slow queries. Fix the N+1 first.
  • Traffic is modest. PHP-FPM with OPcache is fast, simple and well understood. If it's comfortably handling your load, Octane adds operational complexity for a gain nobody will notice.
  • The codebase leans on global or static state, especially an older one, or one with packages you can't audit. You'd be signing up for subtle cross-request bugs.
  • The team hasn't worked with long-running PHP and the app handles sensitive per-user data. Get comfortable in staging first; the bugs here are security bugs.

My take

Octane is a genuinely good tool, and the speed-up is real for the right workload. But it's an optimisation with a new mental model attached, not a free upgrade. Measure where your request time actually goes, fix the cheap things first, and reach for Octane when bootstrapping is the bottleneck, with a team that knows the goldfish is gone.

The Octane documentation has a good section on dependency injection pitfalls; read it before your first deploy.

Have you run Octane in production? What was the first piece of state that surprised you by surviving between requests?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close