Instead of maintaining many crontab lines, Laravel lets you define all scheduled tasks in code — reports, cleanups, backups, reminder emails — and run them from a single cron entry that calls the scheduler every minute.
1. Define tasks
In Laravel 11+, tasks live in routes/console.php (older versions: app/Console/Kernel.php):
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')->dailyAt('06:00')->timezone('Europe/London');
Schedule::command('cache:prune-stale-tags')->hourly();
Schedule::job(new \App\Jobs\SyncInventory)->everyFifteenMinutes();
Schedule::call(fn () => \App\Models\Session::where('expires_at', '<', now())->delete())
->daily()
->name('purge-sessions');
Schedule::command('backup:run')->dailyAt('02:30')
->withoutOverlapping()
->onOneServer()
->emailOutputOnFailure('[email protected]');
Useful frequency methods: everyMinute(), everyFiveMinutes(), hourlyAt(17), daily(), dailyAt('13:00'), weekdays(), weekly(), monthlyOn(1, '04:00'), cron('15 3 * * 1').
2. Add the cron entry on the server
As the user that owns the application (not root):
sudo crontab -u deploy -e
* * * * * cd /var/www/laravel && php artisan schedule:run >> /dev/null 2>&1
Every minute, schedule:run checks which tasks are due and runs them. Use the full PHP path (command -v php) if cron’s PATH doesn’t find it, or a specific version (/usr/bin/php8.3) on servers with several PHP versions. See the crontab guide.
Alternative: schedule:work
php artisan schedule:work runs the scheduler in the foreground, checking every minute — handy in containers where cron isn’t available. Supervise it with Supervisor or systemd like a queue worker.
Alternative: systemd timer
# /etc/systemd/system/laravel-schedule.service
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/var/www/laravel
ExecStart=/usr/bin/php artisan schedule:run
# /etc/systemd/system/laravel-schedule.timer
[Timer]
OnCalendar=*-*-* *:*:00
AccuracySec=1s
[Install]
WantedBy=timers.target
sudo systemctl enable --now laravel-schedule.timer. Output goes to the journal. See systemd timers.
3. Check what’s scheduled
php artisan schedule:list
php artisan schedule:test # pick a task and run it now
php artisan schedule:run -v # run due tasks with output
schedule:list shows each task’s expression and next run time.
4. Prevent overlaps and duplicates
withoutOverlapping()— skip a run if the previous one is still going (uses a cache lock; set an expiry in minutes as argument if tasks may die without releasing it).onOneServer()— with several app servers sharing a cache (Redis/Memcached/database), only one server runs the task. Without it, every server with the cron entry runs every task.runInBackground()— start the task and move on, so long tasks don’t delay others due at the same minute.
5. Output and monitoring
Schedule::command('reports:daily')
->daily()
->appendOutputTo(storage_path('logs/reports.log'))
->onFailure(fn () => logger()->error('Daily report failed'))
->pingOnSuccess('https://hc-ping.com/your-uuid');
pingOnSuccess/pingBefore integrate with heartbeat monitors (Healthchecks.io, Uptime Kuma push monitors), so you’re alerted when a task doesn’t run — the most common silent failure.
6. Time zones
Tasks use the app’s timezone (config/app.php) unless you call ->timezone(). Daylight saving changes can skip or repeat tasks scheduled between 1 and 3 a.m. in affected zones — prefer UTC for the app and schedule in UTC.
Troubleshooting missed runs
- Is the cron entry present for the right user?
sudo crontab -u deploy -l - Does cron run it?
grep CRON /var/log/syslog | tail(orjournalctl -u cron). - Run it manually:
cd /var/www/laravel && php artisan schedule:run -v. - Is the right PHP version used, with the needed extensions?
- Stale overlap lock? Clear with
php artisan schedule:clear-cache. - Config cached with an old schedule? Run
php artisan optimize:clearafter deploys (then re-cache).
Related: Laravel queues with Supervisor and deploying Laravel.