CakePHP Stimulus Plugin
Introduction
Stimulus supercharges your CakePHP application's performance by serving your application using high-powered application servers, including FrankenPHP, RoadRunner, Swoole, and Open Swoole. Stimulus boots your application once, keeps it in memory, and then feeds it requests at supersonic speeds.
Where PHP-FPM boots the framework for every request, Stimulus boots it once inside a long-lived worker and reuses that instance across requests. That is where the speed comes from — and why request-specific state must be reset between requests instead of assumed fresh.
Quickstart
Installing the Plugin
Install via Composer:
composer require crustum/stimulusNOTE
This plugin should be registered in your config/plugins.php file.
bin/cake plugin load Crustum/StimulusTIP
After the plugin registers itself, it's recommended to install the configuration with the manifest system:
bin/cake manifest install --plugin Crustum/StimulusThe Stimulus plugin will create the config/stimulus.php configuration file and append the loading of the config/stimulus.php file to the config/bootstrap.php file:
if (file_exists(CONFIG . 'stimulus.php')) {
Configure::load('stimulus', 'default');
}Alternatively, you can load the plugin in your src/Application.php:
// In src/Application.php
public function bootstrap(): void
{
parent::bootstrap();
$this->addPlugin('Crustum/Stimulus');
}This will enable Stimulus by registering its stimulus install, stimulus start, stimulus reload, stimulus stop, and stimulus status commands and setting up the worker state reset listeners.
All of your application's Stimulus configuration is stored in the config/stimulus.php configuration file:
return [
'Stimulus' => [
'server' => env('STIMULUS_SERVER', 'roadrunner'),
'host' => env('STIMULUS_HOST', '127.0.0.1'),
'port' => env('STIMULUS_PORT', '8000'),
'appClass' => env('STIMULUS_APP_CLASS', 'App\\Application'),
'https' => env('STIMULUS_HTTPS', false),
'max_execution_time' => 30,
'max_requests' => 500,
'garbage' => 50,
'watch' => [
'src',
'config/**/*.php',
'resources/**/*.php',
'templates/**/*.php',
'tests/**/*.php',
'composer.lock',
'.env',
],
],
];Next Steps
Once the configuration is installed, pick a server below and run the installer. Then you're ready to learn more about serving your application and building Stimulus friendly code.
Installation
Stimulus may be installed via the Composer package manager, as shown above. After installing the plugin and publishing the configuration, execute the stimulus install command, which installs the selected server's dependencies, records the server in your .env file, and publishes the plugin configuration through the manifest installer:
bin/cake stimulus installYou will be asked which application server you would like to use (frankenphp, roadrunner, or swoole). To skip the interactive choice, pass the server explicitly:
bin/cake stimulus install --server=roadrunner
bin/cake stimulus install --server=frankenphp
bin/cake stimulus install --server=swooleUse --force to overwrite existing configuration files:
bin/cake stimulus install --server=roadrunner --forceWhen the install succeeds, Stimulus:
- Installs the server dependencies (RoadRunner / FrankenPHP binary, Swoole extension check).
- Appends missing entries to your
.gitignore(rr,.rr.yamlfor RoadRunner;frankenphp,frankenphp-worker.phpfor FrankenPHP). - Writes
STIMULUS_SERVER=<server>to your.envfile (appending it when missing, rewriting it when present). - Publishes the plugin configuration via the manifest installer (
manifest install --plugin Crustum/Stimulus --tag config).
Server Prerequisites
FrankenPHP
FrankenPHP is a PHP application server, written in Go, that supports modern web features like early hints, Brotli, and Zstandard compression. When you install Stimulus and choose FrankenPHP as your server, Stimulus ensures the FrankenPHP binary and the frankenphp-worker.php worker script are installed for you.
bin/cake stimulus install --server=frankenphpCustom Caddyfile Configuration
When using FrankenPHP, you may specify a custom Caddyfile using the --caddyfile option when starting Stimulus:
bin/cake stimulus start --server=frankenphp --caddyfile=/path/to/your/CaddyfileThis allows you to customize FrankenPHP's configuration beyond the default settings, such as adding custom middleware, configuring advanced routing, or setting up custom directives. You may consult the official Caddy documentation for more information on Caddyfile syntax and configuration options.
RoadRunner
RoadRunner is powered by the RoadRunner binary, which is built using Go. When you install Stimulus and choose RoadRunner, Stimulus ensures the RoadRunner Composer packages and binary are installed for you.
bin/cake stimulus install --server=roadrunnerThe installer appends rr and .rr.yaml to your .gitignore when that file exists.
You may customize the worker binary path and HTTP middleware in config/stimulus.php, which is useful for zero-downtime deployments where the application is served from a symlinked current path:
'roadrunner' => [
'command' => env('STIMULUS_ROADRUNNER_COMMAND', 'vendor/bin/roadrunner-worker'),
'http_middleware' => env('STIMULUS_ROADRUNNER_HTTP_MIDDLEWARE', 'static'),
],To use a custom RoadRunner configuration file when starting the server, pass --rr-config:
bin/cake stimulus start --server=roadrunner --rr-config=/path/to/.rr.yamlSwoole
If you plan to use the Swoole application server to serve your Stimulus application, you must install the Swoole PHP extension. Typically, this can be done via PECL:
pecl install swoolebin/cake stimulus install --server=swooleThe installer warns when the Swoole extension is missing. The extension itself is installed outside Composer, so the install still succeeds and the server will refuse to start until the extension is present.
Open Swoole
If you want to use the Open Swoole application server to serve your Stimulus application, you must install the Open Swoole PHP extension. Typically, this can be done via PECL:
pecl install openswooleUsing Stimulus with Open Swoole grants the same functionality provided by Swoole, such as concurrent tasks, ticks, and intervals.
Swoole Configuration
Swoole supports additional configuration options that you may add to your stimulus configuration file if necessary. Because they rarely need to be modified, these options are not included in the default configuration file:
'Stimulus' => [
// ...
'swoole' => [
'command' => env('STIMULUS_SWOLE_COMMAND', ''),
'php_options' => [],
'ssl' => env('STIMULUS_SWOLE_SSL', false),
'options' => [
'log_file' => TMP . 'logs' . DIRECTORY_SEPARATOR . 'swoole_http.log',
'package_max_length' => 10 * 1024 * 1024,
],
],
],The Swoole log file defaults below the application tmp/logs directory, and Stimulus creates that directory before the server boots when it does not exist.
Serving Your Application
The Stimulus server can be started via the stimulus start command. By default, this command utilizes the server specified by the server configuration option of your application's stimulus configuration file (or the STIMULUS_SERVER environment variable):
bin/cake stimulus startYou may override the server, host, and port per invocation:
bin/cake stimulus start --server=roadrunner --host=127.0.0.1 --port=8000By default, Stimulus will start the server on port 8000, so you may access your application in a web browser via http://localhost:8000.
WARNING
Workers hold the booted application in memory. After changing code, providers, environment variables, or configuration, reload the workers (or restart the server, or run with --watch during local development) — otherwise the old code keeps serving requests.
Serving Your Application via HTTPS
By default, applications running via Stimulus generate links prefixed with http://. The STIMULUS_HTTPS environment variable, used within your application's config/stimulus.php configuration file, can be set to true when serving your application via HTTPS. When this configuration value is set to true, Stimulus will instruct CakePHP to generate all absolute links using https://:
'https' => env('STIMULUS_HTTPS', false),bin/cake stimulus start --server=frankenphp --httpsServing Your Application via Nginx
In production environments, you should serve your Stimulus application behind a traditional web server such as Nginx or Apache. Doing so will allow the web server to serve your static assets such as images and stylesheets, as well as manage your SSL certificate termination.
In the Nginx configuration example below, Nginx will serve the site's static assets and proxy requests to the Stimulus server that is running on port 8000:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name domain.com;
server_tokens off;
root /var/virtual/domain.com/webroot;
charset utf-8;
location / {
try_files $uri @stimulus;
expires max;
access_log off;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/virtual/domain.com/logs/server_error.log error;
location @stimulus {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:8000;
}
location = /index.php {
deny all;
}
}When Nginx terminates SSL, set STIMULUS_HTTPS=true so Stimulus generates https:// links, and set your canonical URL:
// In config/app.php
'App' => [
// ...
'fullBaseUrl' => 'https://domain.com',
],# In .env
STIMULUS_HTTPS=trueWatching for File Changes
Since your application is loaded in memory once when the Stimulus server starts, any changes to your application's files will not be reflected when you refresh your browser. For convenience, you may use the --watch flag to instruct Stimulus to automatically reload the server on any file changes within your application:
bin/cake stimulus start --watchUse --poll when watching files over a network where native file events are unreliable:
bin/cake stimulus start --watch --pollYou may configure the directories and files that should be watched using the watch configuration option within your application's config/stimulus.php configuration file.
Specifying the Worker Count
By default, Stimulus will start an application request worker for each CPU core provided by your machine. These workers will then be used to serve incoming HTTP requests as they enter your application. You may manually specify how many workers you would like to start using the --workers option when invoking the stimulus start command:
bin/cake stimulus start --workers=4If you are using the Swoole application server, you may also specify how many "task workers" you wish to start:
bin/cake stimulus start --server=swoole --workers=4 --task-workers=6Specifying the Max Request Count
To help prevent stray memory leaks, Stimulus stops a worker once it has handled 500 failing operations, which keeps a repeatedly failing worker from looping forever. To adjust the recycle behavior, you may use the --max-requests option:
bin/cake stimulus start --max-requests=250The same value is available as the max_requests key in config/stimulus.php. Setting it to 0 disables the limit. Treat this as the last line of defense against leaks, not a substitute for freeing what you allocate.
Specifying the Max Execution Time
By default, Stimulus sets a maximum execution time of 30 seconds for incoming requests via the max_execution_time option in your application's config/stimulus.php configuration file:
'max_execution_time' => 30,This setting defines the maximum number of seconds that an incoming request is allowed to execute before being terminated. Setting this value to 0 will disable the execution time limit entirely. This configuration option is particularly useful for applications that handle long-running requests, such as file uploads, data processing, or API calls to external services.
WARNING
When you modify the max_execution_time configuration, you must restart the Stimulus server for the changes to take effect.
Reloading the Workers
You may gracefully reload the Stimulus server's application workers using the stimulus reload command. Typically, this should be done after deployment so that your newly deployed code is loaded into memory and is used to serve subsequent requests:
bin/cake stimulus reload
bin/cake stimulus reload --server=roadrunnerStopping the Server
You may stop the Stimulus server using the stimulus stop command:
bin/cake stimulus stop
bin/cake stimulus stop --server=swooleChecking the Server Status
You may check the current status of the Stimulus server using the stimulus status command:
bin/cake stimulus status
bin/cake stimulus status --server=frankenphpDependency Injection and Stimulus
Since Stimulus boots your application once and keeps it in memory while serving requests, there are a few caveats you should consider while building your application. For example, long-lived service wiring will only be executed once when the worker initially boots. On subsequent requests, the same application instance is reused, with per-request state reset by the listeners configured under Stimulus.listeners in config/stimulus.php.
Octane-style "register and boot run once" applies here as well: you should take special care when injecting the application service container or request into any object's constructor. By doing so, that object may hold a stale version of the container or request on subsequent requests.
Stimulus automatically resets first-party framework state between requests via its worker listeners (configuration sandbox, router, cache, session, authentication, locale, translator, query log, and more). However, Stimulus does not always know how to reset the global state created by your application. Therefore, you should be aware of how to build your application in a way that is Stimulus friendly. Below, we discuss the most common situations that may cause problems while using Stimulus.
Container Injection
In general, you should avoid injecting the application service container into the constructors of shared (long-lived) objects. If that instance is resolved during the worker boot process, the container snapshot held by the service will be reused on subsequent requests and may miss bindings added later.
As a work-around, you could either scope the service per request (or list it in the flush bindings in config/stimulus.php so the container resolves it again), or you could inject a container resolver closure into the service that always resolves the current container instance.
Request Injection
In general, you should avoid injecting the HTTP request instance into the constructors of shared objects. If the service instance is resolved during the worker boot process, the HTTP request will be injected into the service and that same request will be held by the service instance on subsequent requests. Therefore, all headers, input, and query string data will be incorrect, as well as all other request data.
As a work-around, pass the specific request information your object needs to one of the object's methods at runtime, or read the current request back per request instead of capturing it at construction:
// Prefer this...
$service->handle($request->getData('name'));
// ...over capturing $request in a shared service constructor.WARNING
It is acceptable to type-hint the Cake\Http\ServerRequest instance on your controller methods and route closures.
Do not read PHP superglobals ($_GET, $_POST, $_SERVER) in worker code. They are not reliably populated under workers — use the request object instead.
Configuration Repository Injection
In general, you should avoid capturing the configuration repository instance in the constructors of shared objects. If the configuration values change between requests, that service will not see the new values because it depends on the original repository snapshot.
Stimulus mitigates this with the CreateConfigurationSandbox listener, which sandboxes Cake\Core\Configure per operation. Still, prefer reading configuration at runtime (Configure::read()) after the application has booted, and never resolve configuration in low-level worker boot code before the application is fully bootstrapped.
Managing Memory Leaks
Remember, Stimulus keeps your application in memory between requests; therefore, adding data to a statically maintained array will result in a memory leak. For example, the following controller has a memory leak since each request to the application will continue to add data to the static $data array:
class ReportsController extends AppController
{
protected static array $data = [];
public function index()
{
static::$data[] = bin2hex(random_bytes(5));
// ...
}
}While building your application, you should take special care to avoid creating these types of memory leaks. It is recommended that you monitor your application's memory usage during local development to ensure you are not introducing new memory leaks into your application.
Practical defenses:
- Set a worker recycle limit (
max_requests/--max-requests) as the last line of defense. - Register event listeners in plugin bootstrap, never inside request handlers — the event manager keeps every one.
- Clear static arrays in a lifecycle listener or
flushthe service. Never append to them in request handlers. - Watch for shared services that capture large objects (entities) in closures, since they are held for the worker's lifetime.
Concurrent Tasks
WARNING
This feature requires Swoole.
When using Swoole, you may execute operations concurrently via light-weight background task workers. First read Configure::read('Stimulus.server') to confirm the swoole driver, then resolve tasks through the StimulusManager. You may combine this with PHP array destructuring to retrieve the results of each operation:
use Crustum\Stimulus\StimulusManager;
[$users, $orders] = $manager->concurrently([
fn () => $usersTable->find()->all(),
fn () => $ordersTable->find()->where(['status' => 'pending'])->all(),
]);The concurrently() method accepts an array of closures and an optional wait timeout in milliseconds (default 3000):
$results = $manager->concurrently($tasks, 3000);Results are keyed by their given keys. Concurrent tasks processed by Stimulus utilize Swoole's task workers, which execute within an entirely different process than the incoming request. The number of workers available to process concurrent tasks is determined by the --task-workers directive on the stimulus start command:
bin/cake stimulus start --server=swoole --workers=4 --task-workers=6Rules for task closures:
- Pass scalar IDs and re-fetch entities inside the closure. Do not capture
$this, entities, PDO connections, or other non-serializable state, because closures are serialized for task workers. - Catch exceptions inside closures when you need to transform or log the error before the manager rethrows it as a task exception.
- Without Swoole the same calls resolve sequentially in-process via the
SequentialTaskDispatcher; test correctness, not parallelism.
Ticks and Intervals
WARNING
This feature requires Swoole.
When using Swoole, you may register "tick" operations that will be executed on worker ticks. You may register tick callbacks via the manager's tick method. The first argument is a string key identifying the ticker, the second is the callable, followed by the interval in seconds and whether the callback also runs on the first tick:
$manager->tick('simple-ticker', function (): void {
// ...
}, seconds: 10, immediate: true);On Swoole / Open Swoole hosts the registration returns a due-checked InvokeTickCallable listener with interval bookkeeping; without the extension the callback runs on every received tick instead. Guard tick registration behind the driver check (Configure::read('Stimulus.server') === 'swoole') and test correctness rather than timing when no real server is running.
The Stimulus Cache
WARNING
This feature requires Swoole.
When using Swoole, you may leverage the Stimulus cache store, which is powered by a Swoole table shared across all workers on the server. All data stored in the cache is available to all workers on the server. However, the cached data will be flushed when the server is restarted — treat it as ephemeral, high-frequency storage, not durable caching.
You may set the maximum number of rows as well as the number of bytes per row using the cache configuration options in config/stimulus.php:
'cache' => [
'rows' => 1000,
'bytes' => 10000,
],NOTE
The maximum number of entries allowed in the Stimulus cache may be defined in your application's stimulus configuration file.
Cache Intervals
In addition to the typical get / put operations, the Stimulus cache store (Crustum\Stimulus\Cache\StimulusStore) features interval based caches. These caches are automatically refreshed at the specified interval. For example, the following cache will be refreshed every five seconds:
$store->interval('random', function (): string {
return bin2hex(random_bytes(5));
}, 5);Interval resolvers are serializable closures persisted alongside their refresh metadata; reads resolve pending values through get(), and flushes preserve the interval-* resolver rows so registered intervals survive flushes.
Tables
WARNING
This feature requires Swoole.
When using Swoole, you may define and interact with your own arbitrary Swoole tables. Swoole tables provide extreme performance throughput and the data in these tables can be accessed by all workers on the server. However, the data within them will be lost when the server is restarted.
Tables should be defined within the tables configuration array of your application's stimulus configuration file. An example table that allows a maximum of 1000 rows is already configured for you. The maximum size of string columns may be configured by specifying the column size after the column type as seen below:
'tables' => [
'example:1000' => [
'name' => 'string:1000',
'votes' => 'int',
],
],To access a table, you may use the manager's table method. Table access without the Swoole server raises, and unconfigured table names raise with their name:
$table = $manager->table('example');
$table->set('uuid', [
'name' => 'Nuno Maduro',
'votes' => 1000,
]);
return $table->get('uuid');Rules for tables:
- Pre-size tables at boot (
'name:maxRows'). You cannot resize them, and exceeding the maximum may fail silently. - Use
incr()/decr()for atomic counters — there are no transactions. - Treat tables as volatile: data is lost on worker restart, so never use them as a database replacement.
WARNING
The column types supported by Swoole tables are: string, int, and float.