Contents
- 1 Running High-performance PHP applications with RoadRunner
- 2 What RoadRunner really is (beyond the tagline)
- 3 The mental shift: PHP as a long-running application
- 4 Core benefits for high-performance PHP
- 5 When RoadRunner makes sense (and when it doesn’t)
- 6 Getting RoadRunner into your project without drama
- 7 The .rr.yaml — where performance really starts
- 8 The hidden edges: state, safety, and subtle bugs
- 9 Running RoadRunner in production without losing sleep
- 10 RoadRunner with Laravel, Spiral, and beyond
- 11 The human side: performance as a shared language
- 12 Where RoadRunner fits in the PHP ecosystem today
Running High-performance PHP applications with RoadRunner
There’s a moment every backend developer hits sooner or later.
It’s late. The office is quiet, or maybe you’re at home with a half‑cold coffee next to the keyboard. The production logs are open on one screen, Grafana on another, and somewhere between those two panes of light you’re staring at a single painful metric:
Response times creeping up. CPU pegged. Workers maxed out.
You’re not doing anything “wrong.” Traditional PHP‑FPM has carried most of the web on its shoulders for years. But at some point, for some kinds of applications, you feel the ceiling. Especially when:
- You’re pushing a high-traffic API
- You’re running a framework-heavy stack like Laravel, Symfony, or Spiral
- You’re juggling queues, gRPC, microservices, real‑time updates
And that’s where RoadRunner stops being a curiosity and starts looking like a way out.
Let’s talk about that.
Not just benchmarks and buzzwords — but how it feels to live with RoadRunner, what it changes in your day, and how to use it without burning yourself, your team, or your weekend.
What RoadRunner really is (beyond the tagline)
On paper, RoadRunner is a high-performance PHP application server, load balancer, and process manager written in Go. It manages long‑lived PHP workers and routes requests to them through a very efficient bridge, instead of starting a fresh PHP process for every request.
In practice, that means a few things:
- Your PHP code stays loaded in memory
- Your framework (Laravel, Symfony, Spiral) boots once per worker, not on every request
- PHP workers live longer and handle thousands of requests before being recycled
- The Go layer takes care of HTTP, load balancing, plugins, queues, gRPC, metrics
Benchmarks from different communities show what this looks like under load: RoadRunner often delivers 2–4x higher throughput and clearly lower tail latency compared to tuned PHP‑FPM for framework-heavy applications, thanks to its memory‑resident architecture and event‑driven design.
On real apps, you feel it as:
- Less CPU burned on pure framework bootstrap
- Smoother latency curves when traffic spikes
- More headroom before “everything feels slow”
It’s not magic. It’s just not killing and respawning PHP on every request.
And once you internalize that difference, you start thinking about your PHP application differently too.
The mental shift: PHP as a long-running application
We’re used to PHP’s share-nothing story: each request starts with a clean slate, uses some CPU and memory, then disappears. PHP‑FPM lives in that world. RoadRunner bends it.
Workers still follow a “share nothing” philosophy between each other, but each worker persists in memory, and your application instance persists with it. The server sends incoming requests to a pool of these workers and they handle them sequentially, while the Go side deals with concurrency and I/O at scale.
This changes your mental model in subtle but important ways:
- State doesn’t reset between requests inside the worker
- That can be great (connection pools, caches, preloaded data)
- That can also be dangerous (leaked state, stale data, memory bloat)
If you’ve used Laravel Octane, Spiral, or experimented with Swoole, you already know this dance: sudden speed, followed by the realization that you must be extremely deliberate about what stays in memory.
I remember the first time I switched an API to RoadRunner. The graphs dropped like a rock — CPU down, latency down — and for a short moment everything felt like victory. Then someone reported odd behavior: user A was seeing some data from user B. The culprit? A singleton with mutable state sitting happily in memory, carrying over data between requests.
RoadRunner gives you power. It also expects you to wield it like an adult.
Core benefits for high-performance PHP
If you strip away the hype and look at RoadRunner as a tool, a few core benefits stand out.
1. Heavy frameworks feel light
Framework bootstrapping is expensive. Routing tables, service containers, config loading, autoloading — these are not free. In PHP‑FPM, you pay that cost on every request.
RoadRunner boots the framework once per worker and reuses the environment. For API-first or microservice applications with lots of endpoints and middlewares, that alone can cut your per‑request CPU usage dramatically.
2. Cleaner concurrency model
The Go server handles:
- HTTP/1.1 and HTTP/2
- gRPC
- Queues and background jobs
- Load balancing across workers
- Connection management and timeouts
Your PHP workers run in a controlled pool, with configurable limits on memory, restart policies, and error handling. You’re no longer juggling separate layers with different lifecycles — it’s one cohesive runtime.
3. Plugin ecosystem for real-world workloads
RoadRunner comes with a plugin system that supports:
- HTTP and TLS
- Static file serving
- Queues and pipelines
- gRPC
- Metrics and observers
- Temporal / workflows in more advanced setups
That matters when you’re not just serving HTML pages, but building something closer to a platform where jobs, services, APIs, and workflows all talk to each other.
4. More stable under long, constant load
For apps that don’t sleep — B2B SaaS, payment APIs, telemetry ingest, anything that handles significant throughput over long periods — RoadRunner’s process management and worker recycling tend to give a predictable, resilient behavior.
It’s not the fastest in every synthetic benchmark. But it’s built with long‑running applications in mind.
When RoadRunner makes sense (and when it doesn’t)
Let’s be honest. You don’t always need RoadRunner.
If you’re running:
- A small CRM
- A classic, low‑traffic marketing site
- A moderate Laravel app on shared hosting
then a tuned PHP‑FPM with opcache and preloading can be perfectly fine, cheaper, and simpler to operate. You sleep better when things are boring.
RoadRunner starts to shine when your world looks like this:
- High-traffic JSON APIs
- Heavy Laravel or Symfony with lots of middleware and bootstrapping
- Microservices with gRPC between components
- A mix of HTTP, queues, and background workers in one stack
- Spiral Framework projects, where RoadRunner is the natural runtime
The rule of thumb I share with colleagues:
- If your bottleneck is framework bootstrap, RoadRunner is likely to help.
- If your bottleneck is I/O (dozens of DB queries or external API calls), you might also look at async approaches like Swoole or OpenSwoole.
- If your main problem is lack of tuning, start with PHP‑FPM, profiling, and basic optimization. Don’t use RoadRunner as a band-aid for bad design.
And above all: benchmark your application. Synthetic tests tell a story, but not the one that really matters.
Getting RoadRunner into your project without drama
Picture this:
You’re in a small team. There’s a deadline. You don’t get to rewrite half your stack or switch to an exotic framework. You want RoadRunner, but you also want things to stay sane.
The good news is: RoadRunner is essentially a binary plus a YAML config, plus a PHP bridge.
The usual flow looks something like this (conceptually):
- Install RoadRunner via a Composer helper (for example,
spiral/roadrunner-cliin Spiral projects) - Download the RoadRunner server binary with a CLI tool
- Add a
.rr.yamlconfiguration file describing:- The server command to launch your PHP app (
php app.php,php artisan octane:start, or a custom bootstrap) - RPC settings (how RoadRunner talks to PHP)
- HTTP plugin settings (ports, TLS, headers)
- Worker pool configuration (
num_workers, memory limits, restart strategies)
- The server command to launch your PHP app (
- Start the server with a command like:
./rr serveor a similar CLI depending on your integration
From the PHP side, you configure a bridge:
- If you’re using Spiral, there’s a native bridge package.
- If you’re using Laravel, you can use Laravel Octane with RoadRunner as a driver.
- If you’re using plain PHP, you can wire up a small “front controller” that speaks the RoadRunner protocol.
Everything stays in your codebase. No “magic” modules, no extensions to compile, just binaries and Composer packages.
It feels less like adopting a new language and more like teaching PHP to live differently.
The .rr.yaml — where performance really starts
One of the most quietly powerful parts of RoadRunner is the .rr.yaml file.
Those few lines often determine whether you get a blazing, stable runtime… or a chaotic system with mysterious worker deaths and strange latency spikes.
You’ll typically configure things like:
-
Server command
server: command: "php app.php" relay: "pipes" -
RPC settings
rpc: listen: "tcp://127.0.0.1:6001" -
HTTP plugin
http: address: "0.0.0.0:8080" pool: num_workers: 4 -
Worker supervision
supervisor: max_worker_memory: 100
Those numbers are not just config. They’re trade-offs:
- Too few workers? You underutilize CPU and create queues.
- Too many workers? You waste memory, thrash caches, or hit connection limits.
- Aggressive memory limits? You get frequent worker restarts.
- Too loose? You risk leaks impacting performance over time.
Tuning RoadRunner feels like tuning PHP‑FPM, but with more immediate, visible impact. It’s a good idea to:
- Start with conservative worker counts
- Watch CPU, memory, and latency under real load
- Adjust step by step, not in giant leaps
- Add metrics plugins or external observability (Prometheus, logging) early
You don’t fully know a system until you’ve seen it under stress. That’s doubly true for long‑running PHP.
Let’s talk about the part people often discover the hard way.
When PHP lives longer, any state that’s stored in properties, singletons, static variables, or service containers can persist across requests. That’s both opportunity and risk.
Benefits of stateful workers:
- You can keep database connections open and reuse them.
- You can maintain in-memory caches of configuration, routing, or expensive computations.
- You can preload heavy objects once and amortize their cost across many requests.
Risks of stateful workers:
- A request might modify some object you didn’t intend to be shared.
- Another request might see that mutated object and behave incorrectly.
- Slowly accumulating data can lead to memory leaks over hours or days.
A few practical guidelines that help:
- Treat services as stateless unless they are explicitly designed to be stateful and shared safely.
- Avoid storing per-request data in static variables, global singletons, or long-lived service properties.
- Be cautious with caching in objects — prefer well‑understood cache components (Redis, in‑memory cache with clear eviction).
- Monitor worker memory usage and use supervisor limits to force recycling before issues become severe.
The feeling when you debug one of these bugs is very particular. You get reports that something breaks “sometimes,” over time, but not consistently. Everything works on the first request. Logs look fine. Then you realize you’re living in a long‑running world, and the bug isn’t in what happens once — it’s in what happens after many requests.
RoadRunner doesn’t create that complexity, but it exposes it. And once you start thinking in that space, you’re closer to building truly robust high‑performance applications.
Running RoadRunner in production without losing sleep
Production changes how everything feels.
Benchmarks are fun in local Docker containers. Real users at 3 AM are less fun. To run RoadRunner in production comfortably, you want a few habits in place.
1. Wrap RoadRunner in your usual process tools
- Use systemd, Docker, or your favorite process manager to keep RoadRunner alive.
- Add health checks at the HTTP or gRPC level.
- Use restart policies for the RoadRunner binary itself.
RoadRunner manages PHP workers, but something still needs to manage RoadRunner.
2. Log at both layers
- The Go layer provides logs for plugins, workers, and internal events.
- Your PHP application should log requests, errors, and business logic.
The combination lets you see if an error is:
- A worker crash
- A PHP exception
- A network issue
- A configuration mistake
3. Watch tail latency, not just average
RoadRunner’s strengths are especially visible in P95/P99 latency — that “last 5–1% of requests” where users feel hiccups. When you tune workers, you often see those tails clean up first.
4. Test worker recycling rules
You can set memory or job limits for workers, then automatically recycle them:
- Avoid long-lived memory leaks
- Clear rarely used state
- Keep performance consistent over days, not just minutes
Spend time validating these rules before a big release. Boring is good in production.
RoadRunner with Laravel, Spiral, and beyond
On platforms like Find PHP, most readers live in real ecosystems, not bare PHP scripts. The question is always: “Does this play nicely with my stack?”
RoadRunner has some natural homes:
- Spiral Framework is built around RoadRunner as its runtime. If you’re using Spiral, RoadRunner feels like part of the framework’s DNA.
- Laravel can use RoadRunner via Laravel Octane, which supports drivers including RoadRunner and FrankenPHP. Octane handles a lot of the bootstrapping complexity for you.
- Other frameworks can integrate through the RoadRunner PHP SDKs and custom bridges.
Each ecosystem has its own personality:
- Spiral leans into the idea of PHP as a long‑running, service-oriented architecture. RoadRunner is almost a given.
- Laravel developers often explore Octane when they hit performance walls and want to keep their beloved artisan ecosystem.
- Some teams build custom microservices where RoadRunner is purely an infrastructure piece, and frameworks are thin.
Whatever your route, the same core questions apply:
- How do you boot the app once into a worker?
- How do you keep request handling stateless?
- How do you configure pools, queues, and plugins in a way that matches the architecture?
The deeper you go, the more RoadRunner feels less like “a cool alternative to PHP‑FPM” and more like “the runtime that shapes how we design this system.”
There’s a quiet human angle to tools like RoadRunner.
Teams build trust around performance in strange ways. Someone introduces a new server. Someone else is sceptical, burned by past experiments. Infrastructure folks worry about observability. Product folks worry about risk.
RoadRunner becomes more than a binary; it becomes a conversation:
- Between developers who want speed and clarity
- Between ops who want reliability and metrics
- Between leadership who want cost‑effective scalability
The best implementations I’ve seen weren’t led by someone trying to “prove” RoadRunner’s superiority. They were driven by people who simply sat down, benchmarked carefully, showed both pros and cons, and invited discussion.
They said things like:
- “This reduces our CPU, but we need to be more careful about shared state.”
- “This improves P99 latency, but we must invest in better logging.”
- “This gives us queues and gRPC in one server, but it changes how we deploy.”
RoadRunner doesn’t solve human tension in teams. But it gives you a concrete, technical topic around which to build more honest conversations about trade-offs, complexity, and long‑term direction.
And that’s often more valuable than another percentage point of throughput.
Where RoadRunner fits in the PHP ecosystem today
In the modern PHP landscape — with FrankenPHP, Swoole, OpenSwoole, tuned FPM stacks — RoadRunner is not the only high‑performance option.
It’s one personality among many:
- Worker-based, memory-resident runtime
- Strong plugin system and multi-protocol support
- Deep integration with some frameworks
- Balanced focus on performance and process management
If your world revolves around finding PHP jobs, hiring PHP specialists, or staying current on evolving backends, RoadRunner is one of those acronyms that now shows up in resumes, project descriptions, and architecture diagrams.
On Find PHP, when you see RoadRunner in a profile, it often means:
- This developer has thought about PHP as more than “request + response”.
- They’ve faced real performance constraints.
- They’ve engaged with configuration, tuning, and long‑running lifecycles.
It’s not a badge of superiority, just a hint that they’ve walked a different path with the language we share.
And if you’re the one exploring RoadRunner for the first time, maybe tonight, with logs open and the city quiet outside your window, there’s something quietly motivating in knowing that PHP can grow with you — all the way from humble scripts to high‑performance applications that run day and night, steady and alive.
In that soft glow of the monitor, watching a faster, calmer graph settle after the switch, you realize performance isn’t just numbers; it’s the feeling that your work can breathe a little easier, and that’s a good feeling to carry into whatever you build next.