Open-source project
roadrunner-server/roadrunner avatar
roadrunner-server/roadrunner

RoadRunner: a Go application server that keeps PHP workers alive

🤯 High-performance PHP application server, process manager written in Go and powered with plugins

8,509 stars427 forksGoMIT

At a glance

What is it?
RoadRunner replaces the Nginx plus PHP-FPM pair with a Go process manager that speaks to long-lived PHP workers over a Goridge pipe. It fits teams already running PSR-7 code who want queues, KV and gRPC in the same binary, and it is a poor fit for anyone who cannot restructure a bootstrap file.
Who is it for?
Adopt RoadRunner if you already write PSR-7 handlers and want one Go binary to own HTTP, queues, KV and gRPC instead of stacking Nginx, FPM and separate daemons. Do not adopt it if your application is a stock WordPress or Laravel deployment whose bootstrap you cannot move into a while loop, because the worker model assumes you control the request cycle.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What RoadRunner replaces, and for whom

The README frames RoadRunner as an alternative to the traditional Nginx plus FPM setup. That is the whole pitch. In a classic PHP deployment the web server owns the socket, FPM owns a pool of short-lived processes, and each process builds the framework, opens connections and tears everything down at the end of one request. RoadRunner inverts this. A Go binary owns the socket and the process pool, and the PHP side is a worker that boots once and then loops, waiting for work.

The audience is therefore narrower than "PHP developers". It is teams whose request handling already goes through a PSR-7 request and response object, because the shipped worker example is built on `Spiral\RoadRunner\Http\PSR7Worker` and a PSR-17 factory. Frameworks that expose a PSR-7 entry point can be adapted; a legacy application that echoes HTML from a hundred included files cannot, at least not without writing a new front controller.

The feature list is broad: HTTP(S)/2/3 and fCGI servers, queue drivers for RabbitMQ, Kafka, SQS, Beanstalk, NATS and in-memory, KV drivers for Redis, Memcached, BoltDB and in-memory, OpenTelemetry over gRPC, HTTP or Jaeger, a gRPC server, a Centrifugo-backed WebSocket and broadcast plugin, a distributed lock plugin, and a systemd-like services manager with auto-restarts and an execution time limiter. The value proposition is consolidation: one binary and one `.rr.yaml` instead of a supervisor config per daemon.

How the Go binary and the PHP worker talk to each other

The transport is Goridge, a separate Go module pinned in `go.mod` as `github.com/roadrunner-server/goridge/v4`. The PHP worker calls `RoadRunner\Worker::create()`, which returns a worker bound to that pipe, and `PSR7Worker` wraps it so the loop reads a PSR-7 request and writes a PSR-7 response.

The control plane is separate from the data plane. The `.rr.yaml` sample puts the RPC endpoint on `tcp://127.0.0.1:6001` and the HTTP listener on `0.0.0.0:8080`. The server command is `php worker.php`. So there are three moving parts: the Go process listening on 8080, the PHP processes started from `worker.php`, and an RPC socket on 6001 that tooling uses to query and control the running server.

Plugins are wired through Endure, the dependency-injection container listed as `github.com/roadrunner-server/endure/v2`. Each capability in the feature list is its own module: `http/v6`, `jobs/v6`, `kv/v6`, `grpc/v6`, `centrifuge/v6`, `lock/v6` and so on, all at `v6.0.0-beta` versions in the current `go.mod`. That is the architectural bet. RoadRunner is not a monolith with optional flags; it is a plugin graph, and the binary you download is a particular assembly of that graph. The practical consequence is that a feature you do not configure is still compiled in, and a feature you need from a third party has to be built into a custom binary.

Installing RoadRunner and serving a first request

The README lists several installation routes: pre-built release binaries for OSX, Linux, FreeBSD and Windows, a Docker image, a Composer package, a `.deb` for Debian derivatives, a curl script, Homebrew and Chocolatey. The Composer route is the one that keeps the binary version aligned with your project, so start there. It requires the php-curl and php-zip extensions.

bash
composer require spiral/roadrunner-cli
./vendor/bin/rr get-binary

The README states the server binary will be available at the root of your project. Note the second requirement it gives: php-sockets must be installed to run RoadRunner. Check with `php --modules` before going further, because a missing sockets extension produces a failure that looks like a worker problem rather than an extension problem.

Next, create `.rr.yaml`. This is the sample from the README, with the RPC socket, the worker command, the HTTP address and the log level:

yaml
version: '3'

rpc:
  listen: tcp://127.0.0.1:6001

server:
  command: "php worker.php"

http:
  address: "0.0.0.0:8080"

logs:
  level: error

Then create the worker itself. The README's example is a complete PSR-7 loop that writes a body and responds:

php
<?php

use Spiral\RoadRunner;
use Nyholm\Psr7;

include "vendor/autoload.php";

$worker = RoadRunner\Worker::create();
$psrFactory = new Psr7\Factory\Psr17Factory();

$worker = new RoadRunner\Http\PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);

while ($req = $worker->waitRequest()) {
    try {
        $rsp = new Psr7\Response();
        $rsp->getBody()->write('Hello world!');

        $worker->respond($rsp);
    } catch (\Throwable $e) {
        $worker->getWorker()->error((string)$e);
    }
}

With both files in place, a request to port 8080 should return the string the worker writes. If instead you see an `EOF` error, the README gives a specific diagnostic path: confirm the Composer packages from the installation step are present, then run `php worker.php` directly and read the output. That second step is the useful one, because it removes the Go binary from the equation and surfaces the PHP-side failure on its own.

The worker model is the limitation, not a footnote

Long-lived workers are the source of RoadRunner's speed and its sharpest failure mode. Anything that assumed a fresh process per request now persists. Global state, static caches, open database handles, and container singletons all survive across requests in the same worker. A leaking global grows until the worker is recycled or the process dies. The README does not document rollback behaviour, and it does not enumerate which kinds of state are unsafe to keep; that is left to the framework integration and to the reader.

The services manager described in the feature list mitigates this with auto-restarts and an execution time limiter, but mitigation is not isolation. If your code depends on the reset that process death used to provide, you now have to write that reset yourself.

There is a second boundary. RoadRunner is a PHP application server, so it does nothing for a polyglot backend where only part of the traffic is PHP. And because the shipped plugins are Go modules at `v6.0.0-beta` versions in the current `go.mod`, a team that wants to patch a plugin is maintaining a Go build toolchain and a fork, not editing a PHP file. The `Makefile` shows how small that build is (`CGO_ENABLED=0 go build -trimpath -ldflags "-s" -o rr cmd/rr/main.go`), but it still means Go on the build machine.

RoadRunner against Nginx and PHP-FPM

The honest comparison is not RoadRunner versus another application server. It is RoadRunner versus the setup it says it replaces. Nginx plus FPM is two mature daemons with decades of operational documentation, a process model everyone already understands, and no requirement that your application expose a PSR-7 loop. Its weakness is per-request bootstrap cost and the fact that a queue consumer, a WebSocket server and a metrics endpoint are three more daemons with three more supervisor entries.

RoadRunner trades that familiarity for consolidation and for a persistent worker. You get HTTP, queues, KV, gRPC, WebSockets and metrics from one process tree and one `.rr.yaml`, and you pay for it by owning the worker lifecycle. The Dockerfile in the repository makes the trade explicit: it builds `rr` from `cmd/rr` with `CGO_ENABLED=0`, copies it into an Alpine stage, and creates a non-root `rr` user, so the deployment artifact is a small static binary plus your PHP runtime.

If your application is a standard framework app with a stable front controller, the migration is mostly mechanical. If it is not, Nginx and FPM remain the lower-risk choice, and no amount of plugin breadth changes that.

Maintenance, licensing and what an upgrade actually costs

RoadRunner is MIT licensed, and the Dockerfile carries `org.opencontainers.image.licenses="MIT"`. MIT is permissive, so embedding the binary in a commercial product is not the interesting question. The interesting question is the plugin graph: each plugin is a separate module with its own version, and the current `go.mod` pins a large set of them at `v6.0.0-beta` releases. A beta pin means the API surface between the binary and its plugins can still move, and your `.rr.yaml` is the contract that has to survive that movement.

The repository is not archived, and the last push was on 2026-09-18. Recent releases are `v2025.1.15` on 2026-06-17, `v2025.1.14` on 2026-05-15 and `v2025.1.13` on 2026-04-17, so the binary releases have been roughly monthly while the module tree sits on betas. Practically, an upgrade means reading `CHANGELOG.md`, checking whether your configured plugins changed their config keys, and rebuilding if you forked anything. The README does not document a rollback procedure for a bad upgrade, so keep the previous binary and the previous `.rr.yaml` together.

One more cost that is easy to miss: the documentation lives at `docs.roadrunner.dev`, not in the README. The README is a landing page with a feature list and a minimal example. Anything beyond that, including the full `.rr.yaml` sample it links to, is a separate fetch.

Editorial conclusion

Adopt RoadRunner if you already write PSR-7 handlers and want one Go binary to own HTTP, queues, KV and gRPC instead of stacking Nginx, FPM and separate daemons. Do not adopt it if your application is a stock WordPress or Laravel deployment whose bootstrap you cannot move into a while loop, because the worker model assumes you control the request cycle. Before committing, run `./vendor/bin/rr get-binary` on the target machine and check that `php --modules` lists php-sockets, since the README names php-sockets as a runtime requirement and php-curl plus php-zip as requirements for the downloader itself.

Frequently asked questions

How do I install RoadRunner for a PHP project?

The README gives several routes: pre-built release binaries for OSX, Linux, FreeBSD and Windows, the Docker image `ghcr.io/roadrunner-server/roadrunner:2025.X.X`, a `.deb` package for Debian derivatives, a curl script, Homebrew and Chocolatey. The Composer route is `composer require spiral/roadrunner-cli` followed by `./vendor/bin/rr get-binary`, which places the server binary at the root of your project. That route needs php-curl and php-zip, and running RoadRunner needs php-sockets.

How do I use RoadRunner to serve HTTP requests?

You write a `.rr.yaml` with the RPC listen address, the server command and the HTTP address, then write a PHP worker that loops on `$worker->waitRequest()` and calls `$worker->respond()` with a PSR-7 response. The README's sample config uses `tcp://127.0.0.1:6001` for RPC and `0.0.0.0:8080` for HTTP, with `server.command` set to `php worker.php`. The worker is built from `RoadRunner\Http\PSR7Worker` and a PSR-17 factory.

What does the EOF error mean when RoadRunner starts?

The README treats `EOF` as a signal that the PHP side failed to start or exited. It advises checking that the PHP packages from the Composer installation step are installed, and if that does not help, running `php worker.php` directly and reading the output. Running the worker outside RoadRunner is the documented way to see the underlying PHP error.

Can RoadRunner replace Nginx and PHP-FPM?

The README describes RoadRunner as an effective alternative to the traditional Nginx plus FPM setup, with HTTP(S)/2/3 and fCGI servers compatible with PSR-7 and PSR-17. The difference is that the Go binary owns the socket and the process pool while PHP runs as a long-lived worker rather than a per-request process. That requires an application whose request handling goes through a PSR-7 entry point.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. roadrunner-server/roadrunner on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/roadrunner-server-roadrunner.svg)](https://hysenlabs.com/projects/roadrunner-server-roadrunner)