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

Laravel Short Schedule Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Three steps to a working sub-minute schedule:

composer require spatie/laravel-short-schedule

Then register tasks in routes/console.php (Laravel 11+) — this is the first place to look, and the shortest path to value:

use Spatie\ShortSchedule\Facades\ShortSchedule;

ShortSchedule::command('outbox:flush')->everySecond();

Finally, run the worker:

php artisan short-schedule:run

That command is a long-running ReactPHP event loop, entirely separate from schedule:run. It never touches app/Console/Kernel::schedule(), so your existing one-minute schedule keeps working untouched. In production it goes behind Supervisor (or systemd) with autorestart=true.

Legacy apps (L10 and below) use the kernel method instead — same builder, different entry point:

// app/Console/Kernel.php
protected function shortSchedule(\Spatie\ShortSchedule\ShortSchedule $shortSchedule)
{
    $shortSchedule->command('outbox:flush')->everySecond();
}

First use case to reach for: anything genuinely blocked on the 60-second floor of the native scheduler — a webhook/outbox poller, an external API or socket drain, cache pre-warming ahead of traffic, a lightweight health probe. If once-a-minute is acceptable, use Laravel's native scheduler instead; this package is a separate operational process to babysit and shouldn't be adopted for convenience.

Decision rule before you start: each tick spawns a fresh php artisan process. At everySecond(), that's a full framework boot every second. If the task body is a few milliseconds of work, the boot dwarfs it — in that case write one long-running Artisan command with its own internal loop and schedule that at a coarse interval, or push the work onto a queue.

Implementation Patterns

Frequency vocabulary — everySecond(), everySeconds(30), and sub-second values like everySeconds(0.5). Sub-second only makes sense for trivial shell/exec calls; the process spawn cost makes it pointless for Artisan commands.

Command by signature vs. by class. Passing a class resolves it from the container, which is the clean way to keep schedule files readable and to get IDE navigation:

ShortSchedule::command('sync:orders')->everySeconds(5);
ShortSchedule::command(\App\Console\Commands\SyncOrders::class)->everySeconds(5);

Shell commands. exec() takes any bash string — useful for redis-cli, log rotation, or a curl ping where a full framework boot would be waste:

ShortSchedule::exec('redis-cli ping')->everySecond();

Composite constraints. All constraints must pass; chain them in the order you'd read them, but be aware the ordering has no effect on evaluation cost:

ShortSchedule::command('sync:orders')
    ->environments(['staging', 'production'])
    ->between('09:00', '17:00')       // overflow-safe: between('21:00', '01:00') works
    ->when(fn () => Cache::get('api_healthy'))
    ->everySeconds(5)
    ->withoutOverlapping()
    ->onOneServer();

withoutOverlapping() is the one most people forget. Default behaviour is fire-and-forget: a slow invocation does not block the next tick, which is the package's main advantage over the native scheduler but also the fastest way to stack up forty concurrent copies of the same job. For anything touching a shared resource, add it.

Multi-server fan-out. With several app servers behind Supervisor, every server runs its own loop. Use onOneServer() to collapse to a single runner (requires a lock-capable cache store — Redis, Memcached, database; not array or file on shared-NFS setups).

Events → queue, never inline. ShortScheduledTaskStarting and ShortScheduledTaskStarted fire inside the loop, both exposing $command and the Symfony\Component\Process\Process instance. Use them for lightweight observability — a counter increment, a log line — and immediately hand anything heavier to a job:

Event::listen(ShortScheduledTaskStarting::class, function ($event) {
    Log::channel('short-schedule')->info('starting', ['command' => $event->command]);
});

The Process instance on the event is the extension point for anything richer: you can inspect the command line, or capture exit behaviour in a wrapper command of your own.

Memory discipline via lifetime. Under Supervisor:

[program:short-schedule]
command=php /var/www/artisan short-schedule:run --lifetime=3600
autostart=true
autorestart=true
numprocs=1

The lifetime argument terminates the worker and its child processes after N seconds, releasing any leaked memory; Supervisor brings it straight back. Note the changelog: no default lifetime is set in production (1.4.1), so you must opt in.

Testing. Don't test the schedule itself. Test the underlying Artisan command in isolation; if you must assert on the wiring, swap the ShortSchedule facade (available since 1.6.0) and assert on the recorded commands. The upstream suite runs on Pest 4.

Gotchas and Tips

  • Restart the worker after every schedule change. The README states this explicitly. The schedule is read once at boot; editing routes/console.php on a live server changes nothing until short-schedule:run restarts. This burns people during hotfixes — put the restart in your deploy script next to queue:restart.
  • Constraints and event listeners run inside the loop. They block it. A when(fn () => Http::get(...)->json()) evaluated every second will stall every other short-scheduled task. Cache the truth test result, or compute the condition from a value the loop can read for free.
  • No output capture by default. Command stdout/stderr is not surfaced to you in the terminal in any managed way. If a command fails silently, you'll see nothing — log inside the command, and register the ShortScheduledTaskStarted/Starting events for visibility while debugging. There is no short-schedule:list equivalent for inspection; verify your wiring by temporarily adding a Log::info() or writing to a file in the task body.
  • Duplicate workers multiply execution. Two short-schedule:run processes (a stale Supervisor child plus a manual run) mean every task fires twice. numprocs=1 plus onOneServer() if multiple hosts exist.
  • Full framework boot per tick. Consequences people miss: config/env is re-read each invocation (good for config:clear-free deploys, bad if you expected shared in-memory state), and each task opens its own DB connections. At high frequency, watch connection churn on MySQL.
  • Maintenance mode silently suppresses everything. Commands don't run during down. If a task must keep running (draining a queue, for instance), add runInMaintenanceMode(). This surprises people precisely when they're debugging a deploy.
  • between() timezone behaviour isn't spelled out in the README. Assume it follows the app timezone for the worker process, but verify against your Supervisor environment — a container with TZ=UTC and an app configured for Europe/Brussels will produce windows four hours off. Set TZ explicitly in the Supervisor env if your schedule windows are business-hours-sensitive.
  • Version matrix matters more than usual here. 1.7.0 requires PHP 8.3+, adds Laravel 13, and drops Laravel 10 and 11 — a minor-looking bump that can break an upgrade. Pin deliberately: 1.5.1 is the L10 line, 1.5.2 the L11 line. Also relevant: 1.6.4 fixed PHP binary paths containing spaces (Windows/WAMP-style paths), and 1.6.1 made the package tolerate a missing shortSchedule method — useful if you're rolling it out across a fleet of apps gradually.
  • Extension points worth knowing: the ShortSchedule facade and the container-bound Spatie\ShortSchedule\ShortSchedule object (swap either in tests), the two events' Process handle, and exec() as an escape hatch when you want to bypass the framework boot entirely for a hot-path shell task.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core