Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Worker Laravel Package

roadrunner/worker

Core PHP worker package for RoadRunner that provides the Worker API, payload handling, and stdin/stdout communication with the RoadRunner server. Use it to build custom long-running workers and integrate them with RoadRunner's high-performance Go runtime.

View on GitHub
Deep Wiki
Context7

Getting Started

roadrunner/worker is the PHP-side transport layer for a RoadRunner worker. It does not serve HTTP by itself — it speaks the Go↔PHP binary protocol (goridge) over STDIN/STDOUT, TCP, or a unix socket, and gives you a loop of Payload objects to respond to. Reach for it when you are building a custom worker (custom RR plugin, queue consumer, TCP/gRPC endpoint, sidecar process) rather than a plain HTTP app.

v3.8.x notes: 3.8.0 renamed the package from spiral/roadrunner-worker to roadrunner/worker. Same namespace (Spiral\RoadRunner\*), same API — only the Composer name changed. If your composer.json (or a transitive dependency) still lists spiral/roadrunner-worker, remove it: both packages autoload the same classes, and you'll get duplicate-class / "first one wins" behaviour on upgrade. 3.8.2 then fixed RoadRunner version detection so it is read from the roadrunner/roadrunner package instead of being inferred indirectly — relevant if you (or a plugin) gate behaviour on the server version.

composer require roadrunner/worker spiral/goridge
# version detection in 3.8.2 reads from this package — keep it installed and current
composer require roadrunner/roadrunner
# remove the old name if present
composer remove spiral/roadrunner-worker
# and the binary, usually via the CLI package:
composer require --dev spiral/roadrunner-cli
vendor/bin/rr get-binary

PHP 8.2+ is now required. Laravel 10/11/12 apps are unaffected; a Laravel 9 app still pinned to PHP 8.0/8.1 must stay on roadrunner/worker:^3.7 (or the old spiral/roadrunner-worker) until it upgrades PHP. If Laravel Octane pulls this package in transitively, check composer why roadrunner/worker before assuming your composer update changed anything — Octane's constraint can hold you on the old major line.

Minimal worker:

<?php
use RoadRunner\Worker;
use Spiral\RoadRunner\Payload;

require __DIR__ . '/vendor/autoload.php';

$worker = Worker::create();          // 3.x: (new WorkerFactory())->create()

while ($payload = $worker->waitPayload()) {
    try {
        $result = do_something($payload->getBody(), $payload->getContext());
        $worker->respond(new Payload($result));
    } catch (\Throwable $e) {
        $worker->error($e->getMessage());
    }
}

Register it in .rr.yaml:

version: '3'
server:
  command: 'php worker.php'
  relay: pipes

Then vendor/bin/rr serve. Where to look first: the WorkerInterface / Payload classes and the Worker::create() source (it shows which env vars drive relay selection) — that pair is the whole API surface. The refreshed README in 3.8 is now actually worth reading first, since it reflects the new package name. Your first real use case is almost always a jobs consumer: roadrunner/worker is what spiral/roadrunner-jobs builds on, so a custom RR_MODE=jobs worker is the fastest way to see it working end to end.

Implementation Patterns

Request/response loop with typed payloads. Keep the loop dumb and push logic into a handler you can unit test:

interface Handler { public function handle(Payload $p): Payload; }

while ($payload = $worker->waitPayload()) {
    $worker->respond($handler->handle($payload));
}

Use the header for routing, not the body. $payload->getHeader() (array) and $payload->getContext() carry metadata set by the Go side — queue name, method, status code, trace IDs. Dispatch on the header so you never re-parse the body twice.

Streaming large bodies. For responses bigger than a few MB, wrap the worker in RoadRunner\Stream and write to it (PSR-7-style) instead of buffering the whole body into a Payload. Same for reading: $worker->waitPayload() gives you the full body, so chunk on the Go side if you need true streaming.

Error surface. $worker->error('msg') sends an error frame; the server decides what that means (HTTP 500, job retry, plugin exception). Reserve throw for cases where you genuinely want the worker process to die and be respawned — throwing inside the loop kills the worker, and behaviour depends on max_worker_memory / restart policy.

Graceful shutdown. Never exit. Call $worker->stop() and let the loop end naturally; the Go server then considers the worker retired rather than crashed. Call it on SIGTERM if you register handlers, or after N jobs (a common leak mitigation).

Version-aware behaviour. If you branch on the RoadRunner server version (feature gating, protocol differences, plugin capability checks), source it through the package rather than parsing rr --version or poking env vars yourself. As of 3.8.2 that detection resolves through the roadrunner/roadrunner package, so the dependency must actually be installed — in a trimmed production build (--no-dev plus a hand-curated vendor dir) a missing roadrunner/roadrunner is the first thing to check when version detection returns nothing or falls back. Log the detected version once at boot so a bad detection shows up in your first log line rather than as a weird mid-run branch.

Factory + DI. In a Laravel app, bind WorkerInterface in a service provider via a factory that calls Worker::create() once per process, and keep the loop in an artisan command. Do not construct the worker per job. Because it's a singleton per process, remember to forgetInstance() between loop iterations in tests.

Testing without a server. Implement a stub WorkerInterface that yields a queued array of Payloads and records respond() calls — or just use an anonymous class inside a Pest/PHPUnit test. That gives you a fully offline test harness; the package is thin enough that this is the intended testing strategy.

Composition. For HTTP, don't hand-roll it: use spiral/roadrunner-http (or Laravel Octane), which wraps this package. Use roadrunner/worker directly for anything the official plugins don't cover.

Gotchas and Tips

  • The rename bites on upgrade. 3.8.0 ships the same Spiral\RoadRunner namespace under a new Composer name. If both roadrunner/worker and spiral/roadrunner-worker end up installed (easy: Octane pinned the old one, you require the new one), Composer will happily load two copies of Worker.php and you'll get confusing behaviour rather than a hard error. After upgrading, run composer show | grep roadrunner and confirm only one worker package is present.
  • Version detection moved in 3.8.2. It now comes from the roadrunner/roadrunner package, not from the binary or ambient environment. Two practical consequences: (a) make sure roadrunner/roadrunner is a real, up-to-date dependency — it's easy to drop when slimming vendor or when a transitive dep previously provided it; (b) the detected version is the package's notion of the server version, so if you deliberately run a newer/older rr binary than the PHP package expects, don't trust detection to warn you — see the pinning note below. If detection behaves oddly after upgrading, composer show roadrunner/* is the fastest diagnostic.
  • PHP 8.2 floor. A composer update in a Laravel 9 app on PHP 8.1 will silently hold you at 3.7.x (or fail loudly if you required ^3.8). That's a different code path from the one you're testing on CI if CI runs a newer PHP — pin the same PHP version locally and in CI.
  • #[\Override] on overridden methods. 3.8.0 annotates overrides in the source. On PHP 8.3+ the engine enforces it, so any subclass/decorator of WorkerInterface or Payload whose signatures have drifted will now fatal with "method must be compatible" instead of quietly working. On 8.2 the attribute is inert — so a subclass can pass locally and break in production on 8.3. If you decorate the worker, keep signatures byte-identical to the interface.
  • STDOUT is the wire. Any echo, var_dump, print_r, or stray warning printed to stdout corrupts the goridge frame and you get a Broken pipe / unexpected response with no stack trace. Log to STDERR (php://stderr, Monolog's StreamHandler) and set display_errors=stderr.
  • Output buffering must be off. php -d output_buffering=0 or ob_end_flush() at the top of the worker. BOMs and whitespace before <?php in included files count as output too. Laravel's own bootstrap will happily print a deprecation notice to stdout before your loop starts — run the worker with php -d display_errors=stderr artisan rr:worker.
  • RR_MODE / RR_RELAY matter. Worker::create() reads them to pick the transport (default pipes). If you construct the worker yourself in a context where the plugin hasn't set them, you get a socket/relay mismatch — pass the relay explicitly or ensure the plugin config matches.
  • Pin the versions together. The goridge protocol differs between v2 and v3, and the Go binary must match the PHP package's major line. An upgrade of rr without composer update (or vice versa) shows up as a silent hang or a goridge framing error, not a version warning. With the rename in play, also confirm your rr binary is recent enough to know the new package name in its scaffolds — and remember 3.8.2's version detection reports what roadrunner/roadrunner says, so it can agree with Composer while disagreeing with the binary actually on disk. When in doubt, compare vendor/bin/rr --version with composer show roadrunner/roadrunner directly.
  • Exactly one response per payload. Forget respond() and the worker stalls; respond twice and the second frame is interpreted as the next request's reply. Wrap every branch in try/catch/finally.
  • waitPayload() returning null means EOF — the server closed the pipe (usually during shutdown). That's your queue's exit condition, not an error.
  • Leaks accumulate across jobs. Statics, container singletons, Doctrine's identity map, and unbounded arrays grow forever because the process is long-lived. Cap with max_jobs in .rr.yaml and recycle, or audit for static caches.
  • Debugging: RR_LOG_LEVEL=debug vendor/bin/rr serve -v -d on the Go side; on the PHP side, write a \n-terminated dump to php://stderr. Attach strace/lsof if you suspect the relay rather than the code.
  • Extension points: implement WorkerInterface to decorate (metrics, tracing, timeouts) without touching the loop; subclass Payload if you need lazy body decoding (mind the #[\Override] note above); implement a custom goridge Relay for non-standard transports.
  • Windows: STDIN/STDOUT are opened in text mode by default in some shells — set binary mode or use the TCP/unix relay instead of pipes.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin