spiral/roadrunner-laravel
RoadRunner-powered application server for Laravel: replaces PHP-FPM with a high-performance Go worker, boosting throughput and cutting latency. Includes config publishing, Laravel integrations, and support for HTTP and queues via a simple composer install.
The bridge turns a standard Laravel app into a resident-worker application served by the RoadRunner binary instead of PHP-FPM. Day-to-day, that means three moving parts you need to keep in sync: the Composer package, the rr binary, and .rr.yaml.
Minimal setup:
composer require roadrunner-php/laravel-bridge
./vendor/bin/rr get-binary # download the matching rr binary
php artisan vendor:publish --tag=rr-config # config/roadrunner.php + .rr.yaml
./rr serve # instead of php artisan serve
Where to look first:
.rr.yaml — server-level concerns: http.pool, jobs.pool, kv, temporal, reload, logs. This is the file you'll tune for workers, timeouts, and allocation.config/roadrunner.php — Laravel-level concerns: HTTP middleware/kernel wiring, static file handling, cache store, queue connector, Temporal worker settings.WorkerOptions (max jobs, max tries, backoff, timeouts) — the knobs that decide how long a worker survives and how jobs are retried.The first real use case is almost always "run my existing HTTP app under rr serve and confirm middleware, sessions, and auth behave the same." Do that before touching queues or Temporal — it isolates worker-lifecycle bugs from integration bugs.
If you only need queues, you can skip the HTTP side entirely: configure a jobs pipeline in .rr.yaml, point Laravel's queue connection at the RoadRunner driver, and run ./rr serve as your queue supervisor in place of queue:work.
HTTP workers. Keep the normal Laravel HTTP kernel; the bridge wraps it. Register middleware the Laravel way, but treat anything bound as a singleton as suspect — it survives between requests. Prefer scoped() bindings for request-dependent services, and reset stateful singletons explicitly (auth guards, Request, UrlGenerator roots, view shared data).
Queues. Dispatch jobs with the ordinary dispatch() API and let the RR jobs driver consume them. Two patterns worth adopting:
WorkerOptions::$maxTries as the single source of truth for retry limits, and set it explicitly. Null now falls back to 10 (changed in 6.6.2), so code that relied on an unset value meaning "unlimited" or "Laravel default" will behave differently.backoff: [10, 30, 60]) deliberately; 6.6.2 makes release() honour the array. Before this, an array backoff could collapse into a single value on release.failed(?Throwable $e) defensively. 6.6.2 added handling for jobs that fail without an exception, so your failed() method can now receive null where it previously wouldn't have been called at all.Cache. Use the RoadRunner cache store for hot, short-lived data (rate limit counters, idempotency keys, locks). increment/decrement now preserve the remaining TTL (6.6.2) instead of resetting or dropping it — this matters for sliding-window counters, so re-verify any rate limiter that assumed the old behaviour.
Temporal. Register workflows/activities through the Temporal worker config, and keep activities thin: they run in the worker process and share its lifecycle. As of 6.6.2, exceptions thrown from failed activities are rethrown rather than swallowed, so try/catch blocks in workflow code will start seeing errors they previously missed. That's good — but it will surface latent bugs on upgrade.
Local vs production. Run the same .rr.yaml in both where possible. Use reload in dev so file changes restart workers; disable it in production and use rr reset / a signal-based restart during deploys so in-flight requests and jobs drain within destroy_timeout.
Deployment sketch. Build an image containing the rr binary, run ./rr serve as PID 1, add a health check against the HTTP endpoint, and make sure your orchestrator sends SIGTERM and waits at least as long as destroy_timeout. Pin the binary version alongside the Composer package — mismatches produce confusing protocol errors rather than clean failures.
Testing. Feature tests keep working through Laravel's normal test client; you don't need rr serve for them. Reserve real rr serve runs for smoke tests of middleware, sessions, and queue round-trips. Use Queue::fake() as usual for dispatch assertions.
dd(), exit, die, and uncaught fatals kill the worker. RoadRunner will restart it, but you'll see dropped requests. Use proper exceptions and let the error handler render.php artisan serve is not the dev server here. Use ./rr serve. Mixing the two, or leaving an old worker pool running, produces "config changed but nothing happened" confusion.php artisan config:cache plus a running worker pool means stale config until you restart. Bake config at image build time and restart workers on change..rr.yaml, not PHP. max_request_size, allocate_timeout, destroy_timeout, and pool max_jobs all bite before php.ini limits do. Large uploads and slow endpoints commonly need tuning there.max_jobs interacts with memory. Setting it too high exposes leaks; set it low (e.g. 500) and rely on worker recycling rather than chasing every leak.release() converting delay to seconds. If you previously compensated with millisecond values in your job code, remove that workaround after upgrading or your jobs will be delayed 1000×.$job->payload() yourself, don't assume it's an object../rr serve -d -v for verbose output; rr workers to inspect pool state; PHP application logs go to stderr/stdout depending on config, so check your log driver actually writes somewhere persistent.config/roadrunner.php, custom queue connectors, Temporal activity registries, and worker bootstrappers. Most "the bridge doesn't support X" problems are solvable with a middleware or a connector registration rather than a fork.maxTries being null or on backoff arrays actually still retries the way you expect.How can I help you explore Laravel packages today?