php artisan schedule:run runs whatever is due this minute and exits. Cron calls it every minute as the application's user, so the files it writes stay writable by the web server:
* * * * * cd /var/www/shop && php artisan schedule:run >> /dev/null 2>&1Installed in /etc/cron.d (System Cron Pitfalls), it fired each minute; the heartbeat logged at 08:42:10 and 08:43:09. While developing, schedule:work runs the scheduler every minute in the foreground:
$ php artisan schedule:work INFO Running scheduled tasks. 2026-09-23 08:41:00 Running ['artisan' shop:reconcile] ............. 8s DONE ⇂ '/usr/bin/php8.5' 'artisan' shop:reconcile > '/dev/null' 2>&1 2026-09-23 08:41:08 Running [heartbeat] ........................ 4.16ms DONE
A systemd 142,543 timer (systemd Timers) can replace cron. With sub-minute tasks, schedule:run stays alive all minute, so deploys call schedule:interrupt. With several servers, each runs the cron line and each would send the invoices; onOneServer() lets the first take an atomic lock while the rest skip. It needs a shared default cache store with locks (database, memcached, dynamodb or redis), and closures need a name(). schedule:test runs one task, schedule:pause suspends all and schedule:clear-cache removes stuck overlap locks. A stopped scheduler raises no error, so let ->pingOnSuccess($url) on a frequent task feed an external monitor that alerts on silence.