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

Jobs Laravel Package

roadrunner/jobs

Laravel integration for RoadRunner Jobs that lets you dispatch and process background jobs through RoadRunner queues, with configurable pipelines, workers, and job handlers for reliable asynchronous task processing. Simplifies connecting Laravel jobs to RoadRunner workers and managing queue consumpt...

View on GitHub
Deep Wiki
Context7

Getting Started

RoadRunner Jobs is the PHP client for the RoadRunner jobs plugin — a queue abstraction where the actual brokering (memory, AMQP/RabbitMQ, Kafka, SQS, Redis, NATS, Beanstalk, BoltDB) happens in the Go server, and PHP only produces and consumes task payloads over Goridge/RPC. That architecture drives everything about how you use it: queues are declared in .rr.yaml, not in PHP, and PHP workers are just message handlers.

1. Install and wire up

composer require roadrunner/jobs spiral/roadrunner-worker spiral/roadrunner-cli
vendor/bin/rr get-binary        # pin the binary to your package version

Enable the plugin in .rr.yaml — this is the single most important step, and the place 90% of first-run failures come from:

version: '3'
rpc:
  listen: tcp://127.0.0.1:6001
jobs:
  num_pollers: 10
  pool:
    num_workers: 4
  pipelines:
    emails:
      driver: amqp
      config:
        queue: emails
        exchange: default
      # consumer-side retry/backoff lives here too

2. Producer — dispatch from an HTTP request

use RoadRunner\Jobs\Jobs;
use RoadRunner\Jobs\Task\Factory;

$jobs  = new Jobs(/* RPC or configured client */);
$queue = $jobs->create('emails');          // must exist in .rr.yaml
$queue->dispatch(Factory::create('user.welcome', json_encode($payload)));

3. Consumer — a dedicated worker entrypoint

Create bin/jobs-worker.php and run it as a separate process (rr serve with a jobs-mode pool), never under PHP-FPM. The consumer loop receives a ReceivedTaskInterface, you decode $task->getPayload(), and return/ack or throw to nack.

First use case: offload the slow, retryable, non-user-blocking work — welcome emails, PDF generation, thumbnail resizing, webhook fan-out, third-party API sync. If it must be done before the HTTP response, this package is the wrong tool.

Implementation Patterns

Producer/consumer separation is the whole model. Keep a thin Dispatchable-style service that wraps JobsInterface; handlers live in a separate process, usually deployed separately, often scaled separately per queue.

One queue, one handler, one process pool. jobs.pool.num_workers is your concurrency knob — PHP has no say in it. Configure a distinct pipeline per job family so a slow image-resize backlog can't starve password-reset emails.

Wrap the SDK behind your own interface. JobsInterface and QueueInterface are small and stable-ish; your application should depend on yours, not theirs. This buys you a test double that runs synchronously in the same process, and a migration path if the package's API churns (it does — see below).

interface Bus { public function push(string $job, array $payload): void; }

final class RoadRunnerBus implements Bus {
    public function __construct(private QueueInterface $queue) {}
    public function push(string $job, array $payload): void {
        $this->queue->dispatch(Factory::create($job, json_encode($payload)));
    }
}

Payloads as DTOs with explicit versioning. Serialize to JSON (or symfony/serializer); put a v field and the job class name in the payload so old and new code can co-exist during rolling deploys. Prefer ClassName + version + array over PHP-serialized objects — never serialize() a model, deserialization breaks the moment you rename a property.

Return fast, defer work. Handler should do the unit of work and exit; don't dispatch-and-wait inside a handler. If a task needs fan-out, dispatch children from within the handler — chaining is normal and cheap.

Middleware via decoration. Cross-cutting concerns (metrics, OpenTelemetry traces, logging, tenant context restoration, idempotency-key checks) belong in a decorator around your handler callable or in the consumer loop, not copy-pasted into each handler:

$consumer->serve($worker, function (ReceivedTaskInterface $task) use ($handlers) {
    $ctx = Tracer::start($task->getHeaders());   // header propagation
    try { $handlers[$task->getName()]($task); }
    finally { $ctx->end(); }
});

Headers are your out-of-band channel. Task headers/metadata survive the round trip, so use them for trace IDs, tenant IDs, attempt counters, and routing hints — not the payload body.

Integration with Laravel/Symfony: there is no official queue driver for either. Two realistic routes: (a) keep RoadRunner jobs for infra-heavy work and Laravel's native queue for app jobs, or (b) build a thin Laravel Queue connector that maps push()/later() onto QueueInterface::dispatch() and dispatch($task, $delay) — the delay support is driver-dependent, so verify before promising delay() semantics.

Local dev: run the memory driver in CI and on laptops so the test suite needs no broker; use the real driver only in integration tests. Reproduce production behaviour by keeping the .rr.yaml pipeline block under version control and identical across environments.

Gotchas and Tips

Version churn is the biggest risk. This package has been through a rename (spiral/roadrunner-jobs → roadrunner-php/jobs) and at least two API-shape changes (RPC-handle based clients, then a worker/consumer model; payloads moved toward PSR-7 streams). With 27 stars and no dependents, there is very little community code to copy from and no Stack Overflow corpus — read the source of the exact tag you install, and pin the version in composer.json alongside the matching RoadRunner binary. Server/protocol and client version mismatches surface as confusing "unknown method" or silent hangs rather than clean errors.

"Queue not found" / "pipeline not found" means your .rr.yaml doesn't match your PHP. The plugin's config is the source of truth; creating queues from PHP with driver-specific create-info objects is possible but rarely worth it outside of tests. Keep the queue names in a constants class and assert them against the config at boot.

Delivery is at-least-once. Every handler must be idempotent: unique constraint on (job_id, type), SETNX on a dedupe key, or an upsert. Design for "this ran twice" from day one, because on worker kill/restart/timeout it will.

Exceptions are your ack/nack signal. Throwing causes the task to be nacked and retried per the consumer's retry config; swallowing an exception and returning normally silently drops the message. Conversely, throwing on a permanent error (bad payload, validation failure) burns through retries and ends in the dead-letter path — distinguish retryable (network, 5xx, lock contention) from permanent, and for permanent failures log the payload and return cleanly or route to a DLQ. Dead-letter handling is driver-specific; check whether your driver even supports it before relying on it.

Long-running worker hygiene. PHP workers are reused across thousands of tasks. DB connections go stale, Eloquent/Doctrine identity maps grow, and memory climbs. Reset container-scoped state per task, wrap handlers in DB::reconnect()-style checks, and cap the damage with RoadRunner's max_jobs-style worker recycling rather than trying to write a perfect handler.

Payload size limits are driver-level, not package-level. SQS caps at 256 KB, Kafka has broker defaults, and large blobs make every retry expensive. Store the blob (S3, local disk, DB) and enqueue the reference.

Delay, priority, and max-attempts are not uniformly supported. dispatch($task, $delay) works on AMQP/Beanstalk/SQS-ish drivers and may be ignored elsewhere; priority is a Kafka/AMQP concern. Test on the actual production driver — the memory driver will happily "support" a feature that your broker does not.

Debugging: run rr serve -v -d and set RR_LOG_LEVEL=debug to see the plugin's own view of consumed/rejected messages. When a task disappears, check broker-side (RabbitMQ management UI, SQS console) before blaming PHP — the plugin acknowledges on the Go side. rr jobs-style diagnostics and the RoadRunner metrics endpoint (jobs collector) reveal queue depth and worker saturation that PHP never sees.

Don't construct the client per task. Build Jobs/Queue once at worker boot and reuse it; the RPC channel is long-lived. The same applies to Consumer — creating one per message defeats the polling model.

Extension points: custom queue types/drivers are configured Go-side, so PHP-side extensibility is limited to (a) your own middleware/decorator layers, (b) custom serializers, and (c) a custom Queue/Jobs implementation of JobsInterface for testing or for a second transport. If you need something the plugin doesn't expose (e.g. per-message TTL on a driver that lacks it), the honest answer is to change drivers, not to hack around the SDK.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
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