The Downsides of Coding With Laravel Octane

Laravel Octane was announced some weeks ago with the promise of pushing the framework to the moon in terms of requests served per second. It wasn’t public until today (in beta form) for everyone to test, with first-party Laravel package compatibility (e.g. Nova or Telescope) out of the box.

The way Laravel Octane works to push the boundaries is essentially simple: Instead of “starting” the application each request, it does it fully once and saves it into a shared part of the memory. Once done, Octane will spawn multiple PHP workers that will receive this shared application instance. These processes handle not one but multiple requests at the same time.

As many of you know, PHP is a single-thread process at its core and will still be even after the arrival of PHP Fibers. Laravel Octane does not use PHP Fibers, but alternatively, it runs the application under a custom HTTP server with tighter control over PHP. For the latter, Roadrunner or Swoole is required.

The amazing performance does come with some code caveats.

One Instance To Rule Them All

If you figured out the problem, you’re awesome. But for those who are still lost, this a very simplified version of how it works:

Diagram showing how Laravel Octane works
A very amateurish and simplified example of how Laravel Octane works

When the server starts, the application starts up by registering and booting all the services — even those marked as “deferred,” which we call “bootstrap.” This fresh instance is then shared across all PHP Workers. This is contrary to how a normal PHP process works. Typically, the application bootstraps and is destroyed each time a request goes in and a response goes out.

I’m going to quote this excellent and extensive Diving Laravel article about how Laravel Octane works — specifically how the application and the service singletons are handed out — which will clear your mind immediately:

“Bindings in the container can be registered as singletons, these special bindings are only going to be resolved once in an application lifetime. The resolved instances will be stored in the container cache and the same instances will be re-used during the lifetime of the application.

(…) Instances are resolved by calling $app->resolve(‘singleton’) or $app->make(‘singleton’). So, if you resolve any singletons inside your service providers’ boot or register methods, these singletons will persist. Singletons that are resolved while handling requests won’t persist tho, they will get constructed on every request when running under Octane. You don’t need to worry about those.”

Since the application has been already bootstrapped, the app works at blast speed since there is no need to bootstrap again. Now, imagine the shenanigans: A service singleton will keep the same data for all the requests from anybody! And this also applies to the config repository and the container itself. Bollocks!

If you want to break Laravel Octane, you can even do a simple memory leak by piling up data into a Class static property for each request.

MyLeaker::$pileUp = Str::random(100);

Coding With Laravel Octane in Mind

Now that you know how it works, it’s very easy to understand that the application, as a whole, is essentially the same in all aspects across all requests. Changing the state globally in the bootstrapping will rebound on all requests, and outside of it, it will only work for the current one. The application doesn’t die once the request is out, but it will immediately serve the next one that comes into the server.

With that in mind, here are my recommendations if you plan to create an application with Laravel Octane or create a package that promises the same:

  • It’s safe to register singletons if you expect them to be resolved only once and to stay the same across requests.
  • It’s safe to inject the config if you expect immutability from it.
  • It’s safe to inject the container to resolve a service registered at bootstrap, but not to add or modify something inside it and expect it to persist.
  • Always try to use request() outside controllers.
  • Never use static properties. Use constants instead when possible.

In short, the application state at bootstrapping is different from the application instance at request time. Also, singletons should not be directly tied to requests.

Conclusion

While Laravel Octane is not disruptive in any way, as a developer, you will have to double-check your dependencies for any Laravel Octane incompatibility — especially those who deal with many singletons.

I think it is convenient to tell the developers that your code has been revised in order to work with Laravel Octane in a non-disruptive way. I’m even badging my repositories as “Laravel Octane Compatible” once revised so people can jump in faster and more safely.

Laravel Octane logo

For the rest, Laravel Octane is not a requisite, as many app bottlenecks are bound to unoptimized database queries, slow session stores, or slow external API requests. As always, benchmark your app to check where the slowness comes from, as Laravel Octane may only give you more headaches than solutions.

Source: https://betterprogramming.pub/the-downsides-of-coding-with-laravel-octane-17f4a7a4ea85