Application server12 min read

RoadRunner: a Go application server that manages PHP workers

Learn how RoadRunner, a Go-based application server and process manager, keeps PHP workers alive, and how to reason about state leakage and cleanup in persistent workers.

PHP compatibility and project lifecycle

Unknown. The supplied source does not state supported PHP versions, framework or CMS compatibility, license or support policy. The example uses plain PHP 8.4 only and does not depend on RoadRunner.

Official sources checked 2026-10-22. Browser lessons on this site run PHP 8.5.

What RoadRunner is

According to its official documentation, RoadRunner is a high-performance PHP application server and process manager. It is written in Go and is designed to be extended through plugins. It runs your application as workers, and each worker is an individual operating-system process.

The classification matters. RoadRunner is an application server. It is not PHP itself, and it is not a framework, a CMS, an async library or an alternative interpreter. PHP core is still the language engine that executes your code. RoadRunner is the Go program that starts, supervises and feeds those PHP processes. Framework components and CMS products such as Drupal CMS are separate layers. The supplied source does not describe any framework or CMS integration, so this guide makes no claims about them. It also does not cover Swoole or Open Swoole, which are distinct projects from RoadRunner.

Architecture

RoadRunner manages a pool of PHP workers and routes incoming requests from its plugins to them. Communication uses the goridge protocol. The documentation lists these plugins for running workers and handling request types:

  • HTTP: forwards client HTTP requests to the PHP application.
  • Jobs: passes queued tasks from queue brokers to a PHP consumer.
  • Centrifuge: forwards events from Centrifugo WebSocket clients, with bidirectional communication.
  • gRPC: passes gRPC requests to PHP.
  • TCP: handles raw TCP requests in RoadRunner v2025; the page says v3 does not support it.
  • Temporal: manages workflows and activities.

In the other direction, PHP code can call RoadRunner services over Goridge RPC. The page states that this transport uses Go net/rpc, not gRPC. The listed services are KV (cache), Locks, Service (a dynamic process supervisor), Jobs (dynamic queue pipeline management), Logger and Metrics (submission to Prometheus).

Execution model: classic requests versus persistent workers

In a classic PHP request lifetime, the web server or FastCGI process starts the script, the script runs, and request-scoped state is discarded at the end. Framework bootstrapping happens again on every request.

RoadRunner instead keeps PHP workers alive between requests. The documentation says this can eliminate boot time such as framework initialization. It also says that because a worker lives in memory, open resources persist across requests. This is the central trade-off. The page describes workers as isolated, shared-nothing processes, so one worker failing does not affect the others, and scaling means adding more workers. No benchmark figures are supplied, so this guide makes no speed claims.

State leakage and cleanup

Persistent memory is also a hazard. Anything stored in a static property, a global, a singleton, a long-lived container service or an open resource can survive into the next request handled by the same worker. Typical failures include:

  • A previous user's identity or session data visible to the next request.
  • Request-scoped logs, caches or buffers that grow without bound.
  • Connections or file handles that remain open or go stale.

The practical rule is to separate state into two groups. Process-lifetime state, such as configuration and read-only services, is safe to build once. Request-lifetime state, such as the current user, request data and per-request buffers, must be created fresh or explicitly reset after every request. The example below demonstrates the leak and the reset in plain PHP. It deliberately does not use any RoadRunner package, because the supplied source does not document package names or APIs.

Practical use cases

Based on the plugin list, RoadRunner fits PHP applications that serve HTTP, consume queue jobs, expose gRPC, handle Centrifugo events or run Temporal workflows. It also suits teams that want centralized logging and Prometheus metrics through the Goridge services. Whether a specific framework or CMS can run on it is unknown from the supplied source.

Installation approach

The supplied page is a conceptual overview. It does not give Composer package names, a download command, a Docker image or a .rr.yaml file. Those details are therefore unknown here, and this guide does not invent them. The "What's Next" section points to a PHP Workers page for configuring and running workers, so use the official documentation for the real Composer and .rr.yaml steps. The working example in this guide is a simulation that you can run with only PHP 8.4.

Strengths

  • Workers stay alive between requests, so boot cost can be avoided.
  • Process isolation and a shared-nothing design.
  • One server for several protocols through plugins.
  • Extensible with custom Go plugins.
  • Built-in services for KV, locks, logging and metrics.

Limitations

  • Persistent workers make state leakage a real risk that classic request-per-process PHP does not have.
  • The supplied source gives no PHP version compatibility, license, support policy or deprecation information, so these are unknown. Lifecycle is recorded as unknown.
  • Feature availability varies by RoadRunner version, as the TCP note shows, so check the version you use.
  • Operating it means running a Go server alongside PHP, which adds a component to deploy and monitor.

Deployment notes

Deployment specifics are not in the supplied source. In general, run it as a normal supervised service, keep any development listener bound to 127.0.0.1, and never expose internal worker transports publicly. Verify all configuration against the official documentation and have it reviewed by a human before production use.

Learning track

The learning track uses PHP 8.4, with PHP 8.5 planned. RoadRunner's own PHP compatibility is unknown from the source, so confirm it before pairing it with a specific PHP version. The example is a runtime-style exercise, so run it locally; it is not a browser exercise.

Practical examples

These examples have not been run in a live environment yet. Commands and versions follow the official documentation linked on this page; check them before relying on them, and report a problem if one does not work.

Simulating persistent worker state leakage and cleanup in plain PHP

A plain PHP 8.4 CLI script that imitates a persistent worker handling two requests in one process. It runs once without cleanup, showing a user leaking into the next request, and once with a reset between requests. It does not use RoadRunner or any RoadRunner package, since the supplied source documents no install commands or .rr.yaml.

src/simulate.php

<?php

declare(strict_types=1);

final class RequestContext
{
    public static ?string $currentUser = null;

    /** @var list<string> */
    public static array $log = [];

    public static function reset(): void
    {
        self::$currentUser = null;
        self::$log = [];
    }
}

/**
 * @param array{path: string, user?: string} $request
 */
function handleRequest(array $request): string
{
    if (isset($request['user'])) {
        RequestContext::$currentUser = $request['user'];
    }

    RequestContext::$log[] = $request['path'];

    return sprintf(
        'path=%s user=%s logged=%d',
        $request['path'],
        RequestContext::$currentUser ?? 'anonymous',
        count(RequestContext::$log)
    );
}

/**
 * @param list<array{path: string, user?: string}> $requests
 */
function runWorker(array $requests, bool $cleanup): void
{
    RequestContext::reset();

    foreach ($requests as $request) {
        echo handleRequest($request), PHP_EOL;

        if ($cleanup) {
            RequestContext::reset();
        }
    }
}

$requests = [
    ['path' => '/account', 'user' => 'alice'],
    ['path' => '/public'],
];

echo 'Without cleanup:', PHP_EOL;
runWorker($requests, false);

echo 'With cleanup:', PHP_EOL;
runWorker($requests, true);

Install and run

php --version
php src/simulate.php

Expected behavior

The output lists two lines per run. Without cleanup, the second request shows user=alice and logged=2, which is leaked state. With cleanup, the second request shows user=anonymous and logged=1.

This is only a simulation of the persistent worker concept. It does not start RoadRunner, use goridge, or show real configuration. Requires PHP CLI 8.4. For real RoadRunner installation and .rr.yaml settings, use the official documentation.