spatie/laravel-short-schedule
Schedule Artisan commands at sub-minute frequencies—every second or half-second—using a long-running ReactPHP event loop kept separate from Laravel's native scheduler, so short tasks never delay your regular schedule.
Architecture fit. The package fills a genuine gap: Laravel's native scheduler is minute-granular, and this provides sub-minute cadence down to 0.5s. Architecturally it is one long-lived ReactPHP event-loop process (short-schedule:run), with one periodic timer per task, each firing spawning a detached Symfony\Component\Process via the non-blocking start(). Slow tasks therefore do not delay sibling tasks — notably better isolation than the native scheduler's sequential foreground execution, and fully independent of schedule:run.
The critical implication is that this is not an in-process worker. Every tick cold-boots a fresh Laravel process. At everySecond(), a single task is ~86,400 boots/day. That cost shapes the correct use: it belongs where you need a cheap, frequent trigger, not sustained in-process throughput. The recommended pattern is tick → thin artisan command → dispatch a queued job. Good fit: cache warmers, external API polling, heartbeat/watchdog checks, queue-backlog nudges, metric scrapers. Poor fit: heavy in-process work, state held across ticks, exactly-once semantics.
Integration feasibility. Very low effort. composer require, register via the ShortSchedule facade in routes/console.php (facade added in 1.6.0) or the legacy shortSchedule() Console Kernel method, add a Supervisor program. No migrations, no published config, no new infrastructure. onOneServer() requires an atomic-lock-capable cache store (redis, memcached, database, dynamodb) — verify the driver per environment. exec() covers non-PHP shell tasks. The two events (ShortScheduledTaskStarting/Started) provide hooks, but listeners execute inside the loop and must be non-blocking.
Technical risk (ranked):
schedule:monitor analogue and no short-schedule:list. A task can fail every second indefinitely with nothing surfacing it.when(), between(), environments() and event listeners are evaluated in-loop on every tick. A slow DB call inside when() at 1s cadence stalls every other task. Easy to violate, hard to notice.--lifetime=N terminates the worker and its child processes; deploy and Supervisor bounces do the same. In-flight tasks die mid-execution. Task bodies must be idempotent, short, and restart-tolerant.withoutOverlapping()/onOneServer() depend on cache locks; a killed child can leave a lock until TTL, stalling that task silently.Key questions.
--lifetime value?onOneServer(), and is it shared across all app servers?Stack fit. Standard Laravel app under Supervisor or systemd, PHP 8.3+, Composer-managed dependencies. No conflict with the native scheduler — the two loops are deliberately independent, so short tasks cannot delay the regular schedule and vice versa. It does not touch queue workers, Horizon, or Octane (Octane's long-lived app process is unaffected because spawned commands remain separate processes). exec() makes a small number of non-Laravel shell tasks possible, though plain cron/systemd timers are usually the better home for those.
Migration path. Nothing to migrate from, since sub-minute scheduling is not natively possible. The realistic migration is from the approximations teams use today: while(true) scripts under Supervisor, hand-rolled daemons, over-frequent queue polling, or sleep loops embedded in commands. Replace those one at a time, each as its own PR with an independent rollback (disable the entry, restart the worker). Do not migrate existing minute-level native scheduler tasks into this package — that strictly adds cold-start cost for no cadence gain.
Compatibility. PHP 8.3+ and Laravel 13 on 1.7.x; Laravel 9–11 on 1.5–1.6.x. Verify: cache driver supports atomic locks if using onOneServer(); the PHP binary resolved by the scheduler matches the application's binary (1.6.4 fixed paths containing spaces; check multi-PHP and version-manager setups); process spawning is not blocked by disable_functions or a read-only filesystem — Symfony Process needs a writable temp directory, hence the spatie/temporary-directory dependency. Multi-node deployments must run the worker on every node, with onOneServer() wherever duplicates are unacceptable.
Sequencing.
everySeconds(5) command that just logs, confirm the loop starts under Supervisor and survives a restart.withoutOverlapping() by default to anything non-trivial.routes/console.php with owner comments, one task per PR, a documented --lifetime per environment, and a deploy-pipeline step that restarts short-schedule:run whenever the schedule definition changes (the README is explicit that schedule changes require a restart).Maintenance. Small code surface — a thin wrapper around ReactPHP and Symfony Process — but two recurring chores. The package tracks only the newest Laravel, making it a forced annual upgrade dependency; fold it into the framework upgrade initiative rather than treating it as standalone. And restart-on-schedule-change means schedule definitions are runtime state, not deploy state — the most common route to silent environment divergence. Low bus-factor risk: MIT, stable maintainer, no commercial dependency.
Support. Community only: GitHub issues, no SLA, no paid support for this package, plus a [email protected] channel for vulnerabilities. With 0 dependents and modest stars, expect to be first to hit edge cases; budget for engineers reading the source (it is small) rather than waiting upstream. Keep a fork-and-patch contingency — the license and code size make single-engineer ownership viable.
Scaling. Scales per node, not horizontally by default: N app servers run N loops, so each task executes N times unless onOneServer() is applied. That flag adds a shared-cache round-trip per tick per task — at 1s cadence across several tasks and nodes this is real lock churn on Redis, and it does not pin a task to a specific node. Cost scales with (tasks × frequency × nodes × Laravel boot cost), independent of business volume — predictable, but easy to underestimate on small instances. Cold-booting Laravel every second will show clearly in CPU baselines. Prefer the lowest frequency that satisfies the requirement: 5s instead of 1s is an 80% reduction in overhead.
Failure modes. (1) Worker dies and is not restarted — total silent stop, detectable only via heartbeat alerting. (2) Worker alive but loop blocked — all tasks stall at once, presenting as "no executions" rather than an error; the same alert covers both. (3) --lifetime or a deploy restart kills an in-flight task mid-write — partial state, so require idempotent, resumable bodies. (4) Orphaned overlap/server locks stall an individual task until TTL. (5) Task failures produce no signal — they must be caught and logged inside the artisan command itself. (6) Memory growth in the loop from constraint closures or listeners, mitigated by --lifetime at the cost of (3). (7) Duplicate execution during a rolling deploy where two workers briefly coexist without onOneServer().
Ramp-up. Roughly half a day for an engineer fluent in Laravel's scheduler, most of it spent on operational framing rather than the API. The surface is small: everySecond, everySeconds, between, when, environments, plus withoutOverlapping, onOneServer, runInMaintenanceMode. The genuinely new knowledge is ReactPHP-loop reasoning: constraints and listeners run in-loop and must never block, and spawned processes are detached so nothing returns to the caller. Formalize with a short internal runbook covering Supervisor configuration, --lifetime selection, restart-on-schedule-change, heartbeat alerting, and the "thin command → queued job" pattern. Spatie's three-part video series is a reasonable onboarding artifact for the under-the-hood portion.
How can I help you explore Laravel packages today?