binary-cats/coordinator
Coordinator is a Laravel package that adds a dedicated coordination layer to your app, giving multi-step workflows a clean home so controllers and services stay thin, focused, and easier to test and maintain.
Coordinator solves a narrow but very common problem: running a scheduled/queued command on exactly one server in a multi-node deployment, with more control than Laravel's built-in onOneServer().
composer require binary-cats/coordinatorphp artisan vendor:publish --tag=coordinator-config (check the package README for the exact tag if this one is renamed).config/coordinator.php first — this is where the default coordinator name, lock store, and strategy live. Read this before touching any command wiring.// app/Console/Kernel.php (or routes/console.php in Laravel 11+)
$schedule->command('coordinator:run "reports:generate"')->dailyAt('02:00');
The coordinator:run wrapper is the entry point most developers need on day one. Everything else (custom coordinators, strategies) is progressive enhancement.
First thing to verify after 9.1.0: this release only bumps Laravel dependency constraints. Run composer update binary-cats/coordinator and confirm your laravel/framework version is still inside the supported range before deploying — no code changes are required, but the constraint change is exactly the kind of thing that resolves (or introduces) a conflict with other packages pinning Laravel.
Scheduler wrapping (the 80% case). Wrap the command name in coordinator:run inside your scheduler. The coordinator acquires a lock, the winning node executes the inner command, and the rest no-op. Keep the inner command name quoted so arguments pass through cleanly.
Grouping with multiple coordinators. If you have distinct workloads (e.g. reports vs maintenance) that must be independently coordinated, define separate coordinator names in the config rather than sharing one. This prevents a long-running report from blocking unrelated maintenance tasks.
Choosing a strategy. Use the "random"/spread strategy when nodes are equivalent and you just want one winner. Use a priority/first strategy when a specific node (e.g. your primary) should prefer to run the task. Set this per-coordinator in config.
Shared lock store is mandatory. Configure the coordinator to use a cache store backed by Redis or the database — anything shared across nodes. This is the single most important integration decision; see Gotchas.
Testing locally. On a single machine the "only one server" behaviour is invisible. To exercise it, boot two processes pointing at the same Redis cache store (different APP_URL/queues) and confirm only one logs execution. Set your cache store to array in phpunit.xml so tests never touch a real lock.
Composing with withoutOverlapping(). You can still add ->withoutOverlapping() on the outer scheduled entry if a run can exceed its interval; the coordinator lock handles cross-node mutual exclusion, withoutOverlapping handles same-node re-entrancy.
file/array cache = silent failure. If the lock store is per-node, every node thinks it won the lock and the command runs N times. Confirm your cache.default (or the coordinator's explicit store) resolves to Redis/database in production. This is the debugging step when "it runs on every server."coordinator:list if available in your version) to confirm which node executed and which skipped.config/coordinator.php, run php artisan config:clear locally and remember config:cache in production bakes the values in — a stale cached config is a frequent "why is my strategy still random?" cause.composer update + verify deploy. There are no deprecations or behavioural changes to migrate. Pin the exact version in composer.json if your CI is sensitive to transitive Laravel constraint changes.coordinator:run, the config-driven coordinator classes are the place to subclass/extend for custom election logic — but validate the lock store wiring first, since that's where most extension attempts fail.How can I help you explore Laravel packages today?