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.
What it is: a read-only-first diagnostic package for a specific, high-toil failure class — ShouldBeUnique jobs that silently refuse to dispatch because a stale lock is held in Redis. v0.1.0 ships two Artisan commands and no runtime code paths.
composer require plus auto-discovery. Hard constraints are narrow: PHP 8.3+, Laravel 12/13, Redis cache store, phpredis or predis. This is effectively a modern-stack-only tool — teams on Laravel 10/11 or database/SQS-backed uniqueness cannot adopt it today. Database-store support is stated as planned, so it must be treated as unavailable in any roadmap commitment.uniqueVia(), which is the detail most homegrown scripts get wrong (locks land in a different cache store than the default and the "obvious" redis-cli KEYS check misleads). Reporting the resolved store, lock connection, and full key including cache and connection prefixes is the difference between a 5-minute answer and an hour of guessing which environment's prefix you're staring at.--release prompts before acting and performs an owner-checked delete (compare-and-delete), so a worker that re-acquired the lock between inspection and release isn't silently unlocked. That is the right primitive. Risk remains that the check is not atomic on the Redis side (WATCH/Lua vs. read-then-delete) — worth verifying before permitting it in production.laravel_unique_job: style prefixes, connection/cache prefixes). If a Laravel minor release changes that composition, the tool doesn't crash — it reports no lock held, which is a false negative during an incident. Silent-wrong is worse than broken here.--limit. On large Horizon backlogs this is the difference between a fast answer and a scan that hurts. Whether it uses SCAN or KEYS internally is a go/no-go detail.SCAN/cursor pagination, and what does --limit actually bound — jobs read, or keys scanned?--json output mode for runbooks/automation, and does --release accept a non-interactive confirmation for scripted use?ShouldBeUniqueUntilProcessing, and jobs dispatched in batches/chains where the unique ID differs per dispatch?Stack fit. Target profile: Laravel 12/13, PHP 8.3+, Redis cache store used as the uniqueness backend (directly or via uniqueVia()), Horizon or plain Redis workers. Every other shop should wait — the package has no value where uniqueness lives in the database cache store, and no value on Laravel ≤11 today.
Migration path. There is none, and that is the point. No data migration, no config publish required, no worker restart, no deploy coupling. Adoption is a composer.json line and a composer update. Recommend landing it as require-dev first, then promoting to a production dependency only if on-call runbooks reference it (a tool nobody can run during an incident is a tool that doesn't exist).
Compatibility checks before merge:
CACHE_PREFIX / per-store connection prefixes across local, staging, and prod. Multi-tenant or prefix-per-deploy setups are where the displayed key either saves or wastes the incident.Sequencing (two sprints, low cost):
--release against a lock, then against a lock a worker has just re-acquired, and confirm the owner check refuses. Do not document --release until this passes.--limit guidance, and an explicit rule that --release requires a second pair of eyes and confirmation that no worker is mid-retry.require-dev if the spike passes; pin the version and treat pre-1.0 bumps as reviewable changes.Maintenance. Small surface, single maintainer, pre-1.0. Expect flag and command-name churn; pin exactly and review diffs on upgrade. The real maintenance burden isn't the package — it's the implicit dependency on Laravel's internal cache key format. Budget a check on every Laravel minor upgrade: run the tool against a known-held lock and confirm it still sees it. Add that to the upgrade checklist, otherwise the failure mode is a silently useless diagnostic.
Support. This is the package's core TPM value: it compresses a recurring, high-frustration incident class into one command with a definitive answer (job, unique ID, TTL remaining, owner, exact key, store). The "why" classification — still queued / running / probably crashed / orphaned — is genuinely useful triage vocabulary and matches how on-call engineers actually reason. The TTL-left field is underrated: it tells you whether to wait or intervene, which is the first decision anyone makes.
Scaling. Read cost scales with the queue search, not with lock count. On a Horizon deployment with a deep backlog, --limit is the throttle and the default matters — confirm it's conservative. Push back hard on any implementation that uses KEYS over a large keyspace; a diagnostic tool that degrades production Redis while diagnosing production Redis is a net negative. Encourage a --json path so the check can be automated into alerting ("lock held > 2× expected job duration") rather than remaining manual.
Failure modes to write down:
--release then races a live worker. Owner-checked delete limits but does not eliminate the consequence (typically a duplicate execution).uniqueVia() on a non-default store — handled, but only if the CLI process resolves the same config as the worker (env/config divergence between a jumpbox and a worker pod is a real trap).redis-cli.Ramp-up. Very low: two self-describing command names, no concepts to teach, roughly 15 minutes for an on-call engineer including the runbook paragraph. The training investment is not in the tool but in the judgment around --release — specifically, that "orphaned" and "probably crashed" are inferences and that releasing a lock is a correctness action, not a cleanup action. Recommend one 10-minute walkthrough at the next on-call handover, with a live demo on staging.
Net recommendation: adopt behind a staging spike, ship as require-dev immediately, promote to production once the owner-check release path and the key-format behavior on your Laravel minor are verified. Do not plan against database-store support or Redis Cluster until they appear in a release. Value is narrow but sharp — it targets one of the most expensive-to-diagnose, cheapest-to-fix failure modes in a Redis-backed Laravel queue, and its read-only default plus owner-checked release show appropriate respect for production.
How can I help you explore Laravel packages today?