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

Console Mutex Laravel Package

illuminated/console-mutex

Prevent overlapping Laravel console commands with a simple mutex. This package adds atomic locking to Artisan commands so scheduled or manual runs never execute concurrently, helping avoid duplicate work and race conditions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer:

composer require illuminated/console-mutex

The package is trait-based, so there is usually no service provider to publish or config file to edit — you opt in per command.

  1. Read the README first, then open src/WithoutOverlapping.php (the trait you'll actually use). That single file tells you the exact property names, defaults, and lifecycle hooks (mutex acquisition, release, signal handling) better than any blog post.
  2. Add the trait to the command you want to protect:
use Illuminated\Console\WithoutOverlapping;

class ImportProductsCommand extends Command
{
    use WithoutOverlapping;

    protected $signature = 'import:products';

    public function handle(): int
    {
        // your long-running job
        return self::SUCCESS;
    }
}
  1. Run the command twice in two terminals. The second invocation is either blocked, skipped, or restarted depending on the configured strategy — that's your first "it works" signal.

First real use case: a scheduled artisan command that occasionally takes longer than its cron interval. Without protection, the 10:00 run is still going when the 10:05 run starts, and you get duplicated work, duplicate rows, or two workers hammering the same table. The mutex makes the second run a no-op instead.

Version note: 13.0.0 adds Laravel 13 support and nothing else. The package's major version tracks the Laravel major it targets, so pick the release line that matches your framework version (Laravel 12 → ^12, Laravel 13 → ^13). A major bump here is not evidence of a trait API change — verify against the changelog before assuming breakage.

Implementation Patterns

Protect scheduled commands at the command level, not the scheduler level. Laravel's withoutOverlapping() only works inside the scheduler and is lost the moment anyone runs php artisan import:products manually on a box. Adding the trait to the command itself protects every entry point: cron, manual, supervisor restart, Horizon worker, deploy hook.

Choose the strategy deliberately. The package exposes multiple mutex strategies, roughly:

  • skip — if the mutex is held, exit immediately. Best for idempotent jobs where a missed run is fine (cache warmers, report rebuilds).
  • spinlock — wait for the lock to free, then run. Best when every invocation must happen exactly once (queue drains, migration-style jobs).
  • restart / kill — stop the currently running instance and start fresh. Best for "state refresh" commands where the newest invocation always wins (watchdogs, long-poll loops).

Pick by answering: is it worse to skip a run, or to run twice?

Tune the timeout, don't leave it at the default. A mutex with no timeout means a crashed process leaves a stale lock that blocks everything until the file is removed. Set a timeout that is comfortably larger than your p95 runtime — if a run normally takes 2 minutes, a 10–15 minute timeout is sane. Too tight and you'll kill legitimate runs; too loose and a dead process blocks the command for hours.

Give logically identical commands the same mutex identity. If import:products and import:all both hit the same table, they shouldn't run concurrently. Check whether the trait lets you pin an explicit mutex name/identifier — if so, share it across the sibling commands rather than relying on the command signature.

Use it as a cheap distributed lock for single-server setups. The default filesystem-based mutex is ideal for one box. For multi-server (e.g. several web nodes each with cron), point the mutex at a shared cache store / shared filesystem instead of local disk, or you're locking per-machine, which is not locking at all.

Wire it into deploy and maintenance workflows. Wrap destructive artisan commands (migrate:rollback, backfill scripts) in a mutex so a deploy that triggers a command can't collide with an operator running the same command in another terminal.

Log the block, not just the skip. The most valuable output of this package is knowing that a run was refused. Emit a log line (or an event) on mutex contention so you can later answer "why did the 3am import not run?" — otherwise the mutex is invisible.

Gotchas and Tips

Stale locks are the #1 support ticket. If a process is SIGKILLed or the container is OOM-killed, the mutex acquisition path may not run and the lock can survive. Always set a timeout, and always have a documented one-liner to inspect/clear the mutex location (file path or cache key) for emergencies.

^C / SIGTERM handling matters. Confirm the trait releases on abnormal termination (Laravel's signal handling + the trait's shutdown hook). If your command spawns child processes, ensure the mutex isn't released while a child is still writing.

Filesystem permissions bite in containers. A lock file created by www-data will not be writable by a root-run cron job. Either standardize the user, point the mutex at a shared writable path, or use the cache-based mutex.

Don't double up with the scheduler's withoutOverlapping(). Combining ->withoutOverlapping() with the trait can produce confusing behaviour (Laravel holds one lock, the command tries to acquire another, and the "restart" strategies fight). Use one mechanism; the trait is the stronger of the two because it applies to manual runs.

Timeout semantics ≠ execution timeout. The mutex timeout typically controls how long a waiting/spinlock process will wait — it does not impose a runtime limit on the command. If you also need "kill after N minutes", that's timeout on the scheduler or a separate signal-based guard.

Testing needs the mutex neutralised. In a test suite, parallel or repeated invocations of a command can block on a real lock. Run tests with an array or null cache store (if the mutex is cache-backed), or invoke handle() directly rather than through Artisan::call() when the mutex isn't what you're testing. Write one dedicated test that does assert the mutex behaviour (invoke twice, assert second is blocked).

Beware in queue workers. A mutex-protected command dispatched onto a queue does not protect the job — it protects the command. If the same logic runs via a job, wrap the job in Cache::lock() instead.

Watch out for exit codes. With skip-style strategies the second invocation typically exits non-zero (or with a specific code). If cron mails you on non-zero exits, or your orchestrator restarts on failure, that "skip" will look like an error. Check the returned exit code and adjust your alerting.

Extension points are the trait itself. Because there's no config service provider, customisation means overriding the trait's methods or properties in your command (mutex name, strategy, timeout, storage). Keep those overrides in a small base command class (App\Console\MutexedCommand) so twenty commands don't each redeclare the same five properties.

Version pin loosely but upgrade consciously. 13.0.0 is a compatibility bump (Laravel 13 support) with no documented trait changes, so the upgrade path from 12.0.0 is normally just a composer.json constraint edit — provided you're already on Laravel 13. Conversely, staying on Laravel 12 means staying on ^12; check the changelog for the Laravel major a given release targets rather than assuming the trait API moved, and re-read WithoutOverlapping.php after any bump in case internals shifted quietly.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo