jeffersongoncalves/laravel-queue-consumer
A Laravel package for consuming queued jobs, providing a straightforward queue consumer that processes background tasks and integrates with Laravel's existing queue system. It helps keep application workloads moving smoothly with less custom queue plumbing.
This package turns an external "hub" into the source of truth for queued work: queue-consumer:run pulls a payload, executes it, and reports delivery back via its exit code — 0 means "delivered, drop it", anything else means "not delivered". That contract drives almost everything below.
Minimal steps:
composer require jeffersongoncalves/laravel-queue-consumer (pin ^1.2 — see the Gotchas section, 1.1.x silently loses released jobs).php artisan vendor:publish) and set the hub endpoint + credentials in .env. Confirm the keys by reading config/queue-consumer.php rather than guessing — this is the first file to open.php artisan queue-consumer:run -v. Watch for the process to exit 0 on success. If you see exit 0 and no output, that is the normal path, not a hang.First real use case: a job that can legitimately defer itself — $this->release(60), or middleware like WithoutOverlapping / RateLimited / ThrottlesExceptions. Before 1.2.0 these were traps; on 1.2.0 they are the primary thing to smoke-test, because that is exactly what the release fixed.
One worker process per queue. The new --queue= flag is the headline operational change. A job released by WithoutOverlapping is re-posted to the hub on the queue named by --queue=, which defaults to default. If your hub dispatches to emails, webhooks, and reports, run three supervised consumers:
php artisan queue-consumer:run --queue=emails
php artisan queue-consumer:run --queue=webhooks
php artisan queue-consumer:run --queue=reports
Without this, everything released funnels back onto default and you get a thundering herd on a queue your workers may not even be watching.
Treat release as a first-class control flow. A released job is now round-tripped — re-posted with its original payload and the delay captured by HubJob. So:
public function handle(): void
{
if (! $this->lock->acquire()) {
$this->release(30); // re-enters the hub, comes back in 30s
return;
}
// ...
}
The returned void and bare return matter: don't return $this->release(30) in a void handler, and don't let an exception escape unless you want a non-zero exit.
Make handlers idempotent. A released job is a job the hub will hand out again, and a HubJob re-post is a network call that can itself fail or duplicate. Assume every handle() can run twice with the same payload — guard with WithoutOverlapping, a cache key, or a DB uniqueness check.
Share cache state across consumers. WithoutOverlapping, RateLimited, and ThrottlesExceptions all keep their state in the cache store. With N consumer processes on N hosts, a file or array store gives each process its own private lock and the middleware becomes decorative. Use Redis or the database store, and define named limiters in a service provider exactly as you would for standard queue workers.
Fail loudly by design. The 1.2.0 behaviour is: if the hub refuses the re-post, the command exits non-zero. Do not paper over that — no || true, no 2>/dev/null, no "restart on any exit" loops that hide it. Configure Supervisor with startsecs/stopwaitsecs and alert on FATAL/backoff, otherwise you have reintroduced the 1.1.0 silent-drop with extra steps.
Keep one logical job per payload. Since the hub owns retry state, pushing fan-out into a single consumer job means a partial failure leaves you unable to express which sub-task to retry. Dispatch follow-up work to a separate queue instead.
0, and the hub marked it delivered. No exception, no failed job, no log line on either side. If you ran 1.1.x in production, grep your logs for jobs that were dispatched but never completed — they are gone, and the fix does not recover them.--queue= defaults to default — check your hub. A hub that does not send a queue name is stated to keep working, but a hub that sends one and a consumer that ignores it (because you didn't pass --queue) will disagree. Verify by inspecting what your hub actually posts.SyncJob::release() still only flips a flag. The fix lives in HubJob, which captures the delay that the base release($delay) throws away. If you subclass or mock SyncJob in tests, your test double will not exercise the real re-post path — write at least one integration test against the actual consumer command.$delay argument is now preserved end-to-end. Verify sub-minute and multi-hour delays survive the round trip; timezone or seconds-vs-milliseconds drift here is subtle and only shows up as "retries feel wrong".WithoutOverlapping + release can livelock. If the lock TTL is shorter than the release delay and the holder never finishes, the job can cycle indefinitely. Set expireAfter deliberately and cap attempts at the hub, since the consumer's own $tries is not the authority anymore.HubJob is the seam for adding tracing IDs, attempt counts, or headers to the re-posted payload. Bind a subclass in the container rather than patching vendor code — the hub needs no changes because the payload is posted back untouched, so any metadata you add must travel outside the job payload itself.--queue unless handlers are idempotent and lock-aware; the consumer has no internal coordination between processes.How can I help you explore Laravel packages today?