Classic PHP-FPM starts the interpreter fresh on every request: boots the application, handles one request, and drops everything from memory. Swoole and RoadRunner work differently — the application loads once and stays in memory between requests, similar to Node.js or Go. This speeds processing up several times over, but it requires a different approach to writing code.
How this differs from familiar PHP-FPM
In the FPM model, every request gets a clean state: no leaks between users, but the dependency container, database connection, and class autoloading are all reinitialized from scratch every time. In the Swoole or RoadRunner model, initialization happens once when the worker starts, and after that only the handler code runs on each request — that is where most of the speed gain comes from. The difference from a plain CLI run of PHP was covered in the article on PHP CLI and FPM.
The flip side is global state. Static class properties, singletons, and simply forgotten variables in closures live on between requests and can leak memory or leak one user's data into another's response if the code was not written with a long-lived process in mind.
Installing Swoole as a PHP extension
sudo apt install php8.3-dev php-pear
sudo pecl install swoole
echo "extension=swoole.so" | sudo tee /etc/php/8.3/mods-available/swoole.ini
sudo phpenmod swoole
php -m | grep swoole
Swoole embeds directly into PHP as an extension and adds classes for an asynchronous HTTP server, coroutines, and websockets. For a separate websocket protocol in plain PHP, see the article on WebSockets with Ratchet.
RoadRunner as an alternative
RoadRunner is built differently: it is a separate Go binary that keeps a
pool of PHP workers and talks to them over the Goridge protocol through
stdin/stdout. The PHP code itself stays plain PHP with no special
extension — you only need the spiral/roadrunner-http package
through Composer, described in the article on
installing Composer.
| Setting | Swoole | RoadRunner |
|---|---|---|
| Type | PHP extension | separate Go binary |
| Coroutines | yes, built in | no, workers are single-threaded |
| Laravel/Symfony compatibility | through separate packages | through separate packages |
| Learning curve | higher | lower |
Where memory leaks come from
A long-lived process makes leaks visible within minutes, not weeks, like in regular desktop applications. Check these spots:
- Static arrays and class-level caches that grow with every request and are never cleared.
- A database connection that gets reopened on every request instead of reusing the already open one.
- The global dependency container — if it gets rebuilt on every request, the whole point of a long-lived process is lost.
- Circular references between objects, which PHP's garbage collector does not always clean up in time inside a long process.
Monitoring workers and restarts
Even a correctly written application is worth restarting on a schedule,
just like regular queues — the approach is almost the same as in the
article on Supervisor for
queues. Swoole has a built-in max_request setting that
restarts a worker after a given number of processed requests:
$server->set([
'worker_num' => 4,
'max_request' => 5000,
'task_worker_num' => 2,
]);
Checklist for moving to a long-lived process
- Remove any static properties from the code that accumulate data between requests without an explicit reset.
- Set up a scheduled worker restart through
max_requestin Swoole or an equivalent limit in RoadRunner. - Run load testing before switching production over — rising latency tied to worker uptime will show a leak right away.