Laravel Octane - What, How, and Why
Taylor Otwell already showed a glimpse of Laravel's latest open-source package, Octane, during his Laracon Online talk - but today, the new package is available on GitHub for everyone to beta test.
In this blog post, I'm going to explore Laravel Octane and tell you what it is, how you can install and use it, and why you might need it.
Disclaimer: Laravel Octane is still a beta software and should not yet be used in production!
What is Laravel Octane?
Laravel Octane is an open-source package that will boost your Laravel application performance. Laravel Octane requires PHP 8, so if you're still on 7.x, you need to upgrade your PHP version. Under the hood, Octane makes use of Swoole and RoadRunner - two application servers, that take care of serving and booting up your Laravel application. Why is it faster, you might ask. Let me explain.
With a traditional PHP application that gets served via a webserver like nginx, every incoming request is going to spawn an PHP-FPM worker. This means that each request starts up one individual PHP process, that will run through all the necessary tasks in order to serve that one request.
In the case of Laravel, this means that the Framework needs to be booted, all service providers register their services within the container, all providers get booted themselves, the request goes through a list of middleware classes, hits your controller, a view gets rendered, etc. until we eventually get a response from our server.
With Swoole or RoadRunner in place, we still have a worker for each incoming HTTP request, but they all share the same booted framework. This means that only the first incoming request is going to bootstrap the framework (including all service providers, etc.) while every other request can then make use of a ready-to-go framework. And this is what makes Octane so insanely fast.
Getting started with Laravel Octane
As Laravel Octane is a package, you need to install it as a dependency of your specific application. You can do this via composer:
composer require laravel/octane\
After you installed Octane in your application, make sure to run php artisan octane:install
. This is going to publish the Octane configuration file, as well as add rr
- the RoadRunner binary, to the .gitignore
file.
As I said, Octane is going to publish its configuration file, which looks like this:
return [
/*
|--------------------------------------------------------------------------
| Octane Server
|--------------------------------------------------------------------------
|
| This value determines the default "server" that will be used by Octane
| when starting, restarting, or stopping your server via the CLI. You
| are free to change this to the supported server of your choosing.
|
*/
'server' => env('OCTANE_SERVER', 'roadrunner'),
/*
|--------------------------------------------------------------------------
| Force HTTPS
|--------------------------------------------------------------------------
|
| When this configuration value is set to "true", Octane will inform the
| framework that all absolute links must be generated using the HTTPS
| protocol. Otherwise your links may be generated using plain HTTP.
|
*/
'https' => env('OCTANE_HTTPS', false),
/*
|--------------------------------------------------------------------------
| Octane Listeners
|--------------------------------------------------------------------------
|
| All of the event listeners for Octane's events are defined below. These
| listeners are responsible for resetting your application's state for
| the next request. You may even add your own listeners to the list.
|
*/
'listeners' => [
WorkerStarting::class => [
EnsureUploadedFilesAreValid::class,
],
RequestReceived::class => [
...Octane::prepareApplicationForNextOperation(),
...Octane::prepareApplicationForNextRequest(),
//
],
RequestHandled::class => [
//
],
RequestTerminated::class => [
//
],
TaskReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TickReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
OperationTerminated::class => [
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
],
WorkerErrorOccurred::class => [
ReportException::class,
StopWorkerIfNecessary::class,
],
WorkerStopping::class => [
//
],
],
/*
|--------------------------------------------------------------------------
| Warm / Flush Bindings
|--------------------------------------------------------------------------
|
| The bindings listed below will either be pre-warmed when a worker boots
| or they will be flushed before every new request. Flushing a binding
| will force the container to resolve that binding again when asked.
|
*/
'warm' => [
...Octane::defaultServicesToWarm(),
],
'flush' => [
//
],
/*
|--------------------------------------------------------------------------
| Garbage Collection Threshold
|--------------------------------------------------------------------------
|
| When executing long-lived PHP scripts such as Octane, memory can build
| up before being cleared by PHP. You can force Octane to run garbage
| collection if your application consumes this amount of megabytes.
|
*/
'garbage' => 50,
/*
|--------------------------------------------------------------------------
| Maximum Execution Time
|--------------------------------------------------------------------------
|
| (info) 0 means no maximum limit
|
*/
'max_execution_time' => 30,
/*
|--------------------------------------------------------------------------
| Octane Cache Table
|--------------------------------------------------------------------------
|
| While using Swoole, you may leverage the Octane cache, which is powered
| by a Swoole table. You may set the maximum number of rows as well as
| the number of bytes per row using the configuration options below.
|
*/
'cache' => [
'rows' => 1000,
'bytes' => 10000,
],
/*
|--------------------------------------------------------------------------
| Octane Swoole Tables
|--------------------------------------------------------------------------
|
| While using Swoole, you may define additional tables as required by the
| application. These tables can be used to store data that needs to be
| quickly accessed by other workers on the particular Swoole server.
|
*/
'tables' => [
'example:1000' => [
'name' => 'string:1000',
'votes' => 'int',
],
],
];
Next up, you need to decide for yourself wether you want to use RoadRunner, or Swoole. You can then configure the application server that you want to use by customizing the server
key in the configuration file. This can either be swoole
, or roadrunner
.
RoadRunner
RoadRunner is an application server that is written in Go, that does not have any other dependencies within PHP itself. Choose RoadRunner, if you do not want to install additional PHP extensions. You can install RoadRunner through composer, like this:
composer require spiral/roadrunner
Swoole
Swoole comes with a couple of nice benefits, that RoadRunner can not provide. As Swoole is an extension on top of PHP, PHP itself gains some cool new features, such as "ticks" and "coroutines", which I'm going to cover in a bit. These features are not available with RoadRunner, so if you want to make use of them, you should go with Swoole.
You can install the Swoole extension using:
pecl install swoole
During the installation, you will be asked if you want to have support for HTTP2, curl, JSON, and open_ssl within Swoole. You can safely stick to the default values here (which are off
) as those settings only affect things like coroutines. You will still be able to use curl, JSON, and everything else.
Starting Octane
Once you have installed RoadRunner or Swoole, and defined it in your octane.php
configuration file, you can start Octane and let it serve your Laravel application. The Octane server can be started with:
php artisan octane:start
By default, Octane will start the server on port 8000, so you may access your application in a browser via http://localhost:8000
.
So go ahead, visit that route and watch your Laravel application fly! If you make multiple requests to the app, you can see that the first one is a little bit slower - that's where the framework gets booted, while the other ones are noticeably faster, as they can use the booted framework from memory.
200 GET / .............. 14.23 ms
200 GET / .............. 1.93 ms
200 GET / .............. 2.06 ms
Making code changes
If you now go and make a code change - for example, add a new /test
route - and try to hit that URL, you will receive a 404!
And that's because the request is still using the framework (and all of its routes/code) that was booted once you started the Octane server. So in order to see that code change, you need to restart your Octane server.
Because this is super cumbersome to do during development, Octane comes with a nice way to automatically watch your codebase for changes and restart the Octane server automatically.
In order to make this work, make sure to install Chokidar - a NodeJS based file watching library:
npm install --save-dev chokidar
You can then start the Octane server in "watch" mode, like this:
php artisan octane:start --watch
Now the next time you make a change in your codebase, this will be detected, and Octane will restart the workers for the requests and you can immediately see your changes.
Customizing Workers
Speaking of Workers - by default, Octane is going to start one worker for each CPU core that you have. But you can also change this, by passing a --workers
option to the octane:start
command:
php artisan octane:start --workers=2
Swoole specific features
As I mentioned, Octane comes with a couple of Swoole specific features, so lets take a look at those, as I think they are very interesting.
Concurrent Tasks
Octane allows you to perform multiple tasks concurrently. This means that they will be performed at the same time and will be returned as soon as all tasks finished.
Here's an example taken from the Octane documentation on GitHub:
use App\User;
use App\Server;
use Laravel\Octane\Facades\Octane;
[$users, $servers] = Octane::concurrently([
fn () => User::all(),
fn () => Server::all(),
]);
So in this example, we are getting all users and all servers at the same time. To make this clearer, here's another example:
Octane::concurrently([
function () {
sleep(0.5);
return 1;
},
function () {
sleep(1);
return 2;
},
]);
We are executing two "tasks" concurrently and PHP will continue evaluating the code as soon as both of the tasks are finished. One task waits for 0.5 seconds, the other one waits for 1 second. As they are being evaluated concurrently, in two individual tasks, PHP will wait exactly 1 second (not 1.5) until both results are available. This feature is a great way to perform multiple smaller tasks simultaneously.
Just like the --workers
option, you can also customize the amount of --task-workers
that Octane should make available.
Ticks / Intervals
Octane in combination with Swoole, allows you to register ticks
- which are operations that will automatically be executed at a given interval. Similar to the setInterval
method in JavaScript.
Unfortunately, there's no way to stop those ticks at the moment, but you can register them within your application like this:
Octane::tick('simple-ticker', fn () => ray('Ticking...'))
->seconds(10);
The Octane Cache
Another new feature within Octane and Swoole is a new cache driver. This cache driver, according to the official documentation, provides read and write speeds of up to 2 million operations per second. Behind the scenes, Swoole is caching the data in a shared memory using Swoole Tables, which can be accessed across all workers. When the server restarts, the cached data will be flushed though, as the cache is only persisted in memory.
To make use of this cache, you can access it through the octane
cache store on the Cache facade, like this:
Cache::store('octane')->put('framework', 'Laravel', 30);
Another cool new addition, that is Swoole and Octane specific is the ability of a "cache interval". This allows you to store information in the Octane cache and refresh the data in a given interval:
use Illuminate\Support\Str;
Cache::store('octane')->interval('random', function () {
return Str::random(10);
}, seconds: 5)
Octane Tables
Built upon the feature of Swoole Tables, you can create your own tables that you want to access within your Octane applications. These tables have the same performance benefit as a Cache would have, by allowing you to save data in a structured way. Keep in mind that all data that you store within such a table will be lost when the server restarts though.
To configure a table, you can create an entry in the tables
section of your octane.php
config file:
'tables' => [
'example:1000' => [
'name' => 'string:1000',
'votes' => 'int',
],
],
In this example, we are defining a table called example
, which can hold a maximum of 1.000 entries/rows. The structure of this table is a name
, which is a string with a maximum length of 1000, and votes
, which is an integer.
To write data to this table, we can make use of the Octane::table
method:
use Laravel\Octane\Facades\Octane;
Octane::table('example')->set('a-unique-identifier', [
'name' => 'Marcel Pociot',
'votes' => 1000,
]);
And to pull out the data, we can use a get
method on the table, like this:
return Octane::table('example')->get('a-unique-identifier');
Caveats with Octane
There are a couple of things that you need to watch out for, when you want to either make an existing application ready for Octane, or start building a new application from scratch.
Since Octane holds the framework in memory across all workers, things like all of your applications service providers will only be registered and booted once. While Octane takes care of resetting the state of first-party packages (which includes Inertia), it's not possible for Octane to reset global state that you might have in your own application code.
The official documentation, which currently can be found on GitHub holds some of the most common scenarios that you should watch out for.
Listeners
One feature of Octane that hasn't been documented yet, is the ability to register custom listeners, whenever something happens in the application server within Octane. You can hook into the following events:
- WorkerStarting
- RequestReceived
- RequestHandled
- RequestTerminated
- TaskReceived
- TickReceived
- OperationTerminated
- WorkerErrorOccurred
- WorkerStopping
To attach listeners to these events, you can add them to your octane.php
config file.
Service Warming and Flushing
When a new Octane worker gets booted, you can specify a list of container bindings/services that you want to "warm up" during the boot process. This means that, upon the booting of the worker, the service container will already make the specified services available, so that following requests can immediately access them.
Octane already has a list of internal services that it keeps warm during each worker boot process, but you can add your own services to the warm
section of the octane.php
config file.
Similar to this, you can also define a list of services that you want to flush, before a new request comes in. This can be useful for services that get manipulated over the course of a request, that you want to reset to its original/unloaded state when a fresh request comes in.
Octane Routes
If Octane does not already grant you enough speed boost, you can even squeeze a little more out of it, by making use of routing built right into Octane. You can define a custom Octane route through the Octane facade like this:
Octane::route('GET', '/fast', function() {
});
These routes are very fast because they entirely skip Laravels routing system (so these routes do not provide any kind of middleware) - which can be helpful for endpoints that only need to provide data really fast.
Since the HTTP Kernel in Laravel is not being used for these requests, you need to return a Symfony Response object yourself, like this:
use Symfony\Component\HttpFoundation\Response;
Octane::route('GET', '/faster', function() {
return new Response('Hello from Octane.');
});
All service providers are of course booted and available, so that you can still make use of these services, perform Eloquent queries, etc.
Alright...so why Octane?
Laravel Octane is definitely going to give your Laravel application a big performance boost - and we all love performance boosts, don't we? Do we actually need this performance boost? Well, maybe - I think it depends on the application that you are running. But what's more important to me, is the fact that Laravel is (once again) pushing the current state of PHP. Not only is Octane a package that requires at least PHP 8, but it also pushes exciting new features in the PHP world such as coroutines, ticks, and in general the mindset of serving your own application using an artisan command.
I'm excited for the future of Octane!