mohannadelemary/laravel-unique-job-locks
Prevent duplicate Laravel queue jobs from running at the same time. This package adds unique locks to dispatched jobs so identical tasks are skipped or deduplicated until the first one finishes. Simple setup, configurable lock keys and TTLs, and support for Redis and other cache stores.
ShouldBeUnique job that silently refuses to dispatch because a stale lock is held — normally gets diagnosed by an engineer SSH-ing into Redis and hand-decoding cache/connection prefixes. That's an afternoon of senior time per incident. Two read-only Artisan commands are a very cheap "buy," and evaluating them forces the useful question of whether you should instead build lock introspection into your own internal ops tooling.ShouldBeUnique conventions. The why command surfaces the resolved uniqueVia() store, lock connection, and exact Redis key including cache and connection prefixes. That is decision-support for whether your teams should standardize uniqueVia(), how you name unique IDs (tenant-scoped vs. global), and whether you should be setting uniqueFor TTLs deliberately rather than by default.queue:clear) — maps directly onto support escalation scripts. It converts an ambiguous "the job isn't running" ticket into a classified cause, which affects staffing and on-call expectations.--release, and a check that the lock owner hasn't changed since inspection are exactly the guardrails you'd want before letting a broader set of engineers touch production locks. That shapes whether you permit self-serve remediation by support engineers or restrict it to on-call.--job= filtering and --limit= on the queue search matter for whether it's usable against large production queues.Consider it when most of these are true:
ShouldBeUnique or ShouldBeUniqueUntilProcessing in production, and "the job didn't dispatch" is a recurring or feared incident class.--release owner-change check are the differentiating features versus a generic Redis key dump.Look elsewhere, or wait, when:
--release asks before acting, which is the right safety default but is a poor fit for automation. Treat it as an interactive operator tool.Adoption guidance: treat v0.1.0 as a trial. Pin the version, install it in staging first, and validate the why output against a lock you create intentionally. The --release path in particular deserves a staging rehearsal — it's the only mutating behavior and the owner-change guard is worth confirming empirically before anyone runs it against a shared production queue.
For executives: This is a small, free, open-source command-line tool that shortens the time to fix a specific class of production incident — background jobs that quietly fail to run because a stale lock is stuck in Redis. Those incidents are invisible in logs, look like product bugs to customers, and typically burn senior engineering hours in manual Redis archaeology. The cost is one Composer dependency and two Artisan commands; there is no application code change, no new service, no schema change, and no data migration. It is read-only by default, and the one destructive capability is gated behind an interactive confirmation and an ownership check. It is early-stage software (v0.1.0, one maintainer, Redis-only, Laravel 12/13 and PHP 8.3+), so the recommendation is a bounded trial in staging and a single production pilot — not a broad rollout. Framed on the roadmap, it belongs under operational tooling and reliability, not product features; the honest justification is MTTR reduction on queue incidents and a cleaner support escalation path, and the honest risk is maintainer and maturity risk for a tool we could replace with our own script if it stalls.
For engineering: Two commands, queue:unique-locks and queue:unique-lock:why {job} --id=. The first lists held locks with job, unique ID, TTL remaining, and owner, with --job= to filter by class. The second explains a single lock: the resolved store (uniqueVia() is respected — so this works with your existing per-job configuration), the lock connection, and the exact underlying Redis key including cache and connection prefixes, which is precisely the part that makes manual debugging tedious. It classifies the cause as still queued, worker running, worker probably crashed, or orphaned (including after queue:clear) — note "probably crashed" is an inference from lock state, not a heartbeat, so treat it as a hypothesis to confirm. --limit= bounds how many jobs the queue search reads, which is what makes it safe against a large production queue. Both commands are read-only by default. --release prompts before doing anything and refuses if the lock owner has changed since inspection, which handles the race where a job picks the lock up between your inspection and your cleanup. Requirements are Redis cache store with phpredis or predis, PHP 8.3+, Laravel 12/13; database-store support is planned but not available. Plan: pin the version, exercise it in staging against a deliberately orphaned lock, confirm the why output matches what you injected, rehearse --release, then pilot on one production environment with the team that owns the on-call rotation. Expect to revisit if the database store lands or if the package goes unmaintained.
How can I help you explore Laravel packages today?