Instead of a crontab line per task (cron and crontab), the schedule lives in routes/console.php. Schedule::command(), job(), call() and exec() run commands, jobs, closures and shell commands; shop:reconcile is a closure command that sleeps 8 seconds:
Schedule::job(new RebuildBestsellers)->hourly()->onOneServer();
Schedule::command('shop:reconcile')->everyMinute()->withoutOverlapping(10);
Schedule::command('shop:invoices')->monthlyOn(1, '02:00')->onOneServer();
Schedule::command('queue:prune-failed --hours=168')->dailyAt('03:15');
Schedule::command('queue:prune-batches --hours=48')->daily();
Schedule::command('horizon:snapshot')->everyFiveMinutes();
Schedule::call(fn () => Log::info('scheduler heartbeat'))->name('heartbeat')->everyMinute();$ php artisan schedule:list 0 * * * * App\Jobs\RebuildBestsellers ..... Next Due: 19 minutes from now * * * * * php artisan shop:reconcile ...... Next Due: 15 seconds from now 0 2 1 * * php artisan shop:invoices ........... Next Due: 1 week from now 15 3 * * * php artisan queue:prune-failed --hours=168 Next Due: 18 hours from now 0 0 * * * php artisan queue:prune-batches --hours=48 Next Due: 15 hours from now */5 * * * * php artisan horizon:snapshot ..... Next Due: 4 minutes from now * * * * * heartbeat ....................... Next Due: 15 seconds from now
Frequencies run from everySecond() to yearlyOn(); weekdays(), between('8:00', '17:00'), when() and environments('production') narrow them. Times follow app.timezone unless you set schedule_timezone; zones with daylight saving time can run a task twice or skip it. withoutOverlapping() skips a run while the last one continues, via a cache lock that expires after 24 hours unless you give minutes. A second schedule:run, started three seconds into a first one busy with the reconcile, ran only horizon:snapshot and heartbeat. Due tasks run one after another, so the first run's snapshot waited eight seconds; runInBackground() keeps a slow command() or exec() task from holding up the rest.