Laravel Task Scheduling Reference
A task scheduling reference for Laravel's scheduler: running commands and closures on a recurring basis.
Laravel Reference
Defining scheduled tasks
Since Laravel 11, scheduled tasks are defined directly in routes/console.php using the Schedule facade — there's no app/Console/Kernel.php to edit. (Older apps upgraded from Laravel 10 or earlier may still define theirs in the Kernel's schedule() method.)
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->daily();
Schedule::call(function () {
DB::table('recent_users')->delete();
})->daily();
Schedule::job(new ProcessPodcasts)->everyFiveMinutes();
Frequency options
| Method | Runs |
|---|---|
everyMinute() | Every minute. |
everyFiveMinutes() | Every 5 minutes (also 10/15/30 variants). |
hourly() | Once per hour, on the hour. |
hourlyAt(17) | Once per hour, at 17 minutes past. |
daily() | Once per day at midnight. |
dailyAt('13:00') | Once per day at the given time. |
twiceDaily(1, 13) | Twice per day, at the given hours. |
weekly() | Once per week (Sunday at midnight by default). |
weeklyOn(1, '8:00') | Once per week, on the given day/time. |
monthly() | Once per month, on the 1st. |
quarterly() / yearly() | Once per quarter / year. |
cron('* * * * *') | A raw cron expression, for anything the helpers don't cover. |
Constraining when a task runs
Schedule::command('emails:send')
->daily()
->weekdays()
->between('8:00', '17:00')
->environments(['production']);
Schedule::command('reports:generate')
->daily()
->when(fn () => DB::table('orders')->exists());
Preventing task overlap
If a task might still be running when its next scheduled run fires (e.g. a slow import), guard against overlapping executions:
Schedule::command('reports:generate')
->dailyAt('02:00')
->withoutOverlapping();
// On a multi-server deployment, make sure only one server runs it
Schedule::command('reports:generate')
->dailyAt('02:00')
->onOneServer();
// Don't block the scheduler while a long task runs
Schedule::command('import:large-dataset')
->daily()
->runInBackground();
Running the scheduler
Laravel's scheduler still needs exactly one real cron entry to check what's due every minute — individual tasks are never registered as separate cron jobs themselves.
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
For local development, run the scheduler in the foreground instead of waiting on real cron:
php artisan schedule:work
The single cron entry is the most common source of confusion for newcomers: adding a new Schedule::command() line never requires touching crontab again, since schedule:run is what re-reads routes/console.php every minute and decides what's actually due. If nothing seems to fire, the cron entry itself — not the schedule definitions — is usually the first thing to check.
Timezones
By default, scheduled times are evaluated against the server's configured timezone (config('app.timezone')). If a task needs to run at a specific local time regardless of where it's deployed, set the timezone per task:
Schedule::command('reports:generate')
->timezone('Europe/London')
->dailyAt('09:00');
A single default can also be set for every scheduled task via schedule_timezone in config/app.php, which avoids repeating timezone() on each call. Watch for daylight saving transitions on servers not running UTC — a task scheduled with dailyAt() can appear to skip or double-fire around the clock change unless the timezone is set explicitly.
Maintenance mode and failures
Scheduled tasks are skipped by default while the application is in maintenance mode, since most sites don't want background jobs mutating data mid-deploy. A task that must still run — a health check, for instance — can opt out of that behaviour:
Schedule::command('healthcheck:ping')
->everyMinute()
->evenInMaintenanceMode();
To be notified when something goes wrong, chain a callback onto the task itself rather than relying on someone to notice a silent failure:
Schedule::command('reports:generate')
->daily()
->onSuccess(fn () => Log::info('Report generated'))
->onFailure(fn () => Notification::route('mail', 'ops@example.com')
->notify(new ScheduledTaskFailed('reports:generate')));