Task Scheduling
Most real applications need things to happen on a recurring schedule: sending a daily digest email, pruning old records, refreshing a cache every hour. The traditional Unix approach is to add a separate crontab entry for every single recurring task, which quickly becomes hard to track — the definitions live on the server itself, outside version control, and you have to SSH in just to see what's scheduled. Laravel's task scheduler solves this by letting you define every scheduled task as ordinary, readable PHP code, sitting in your project alongside everything else, version-controlled like any other file.
Where the Schedule Is Defined
Depending on your Laravel version, the schedule is defined in one of two places — both are valid, and the fluent API used inside them is identical:
| Location | Used in |
|---|---|
routes/console.php | Newer Laravel versions, using the Schedule facade directly |
app/Console/Kernel.php, inside a schedule(Schedule $schedule) method | Older Laravel versions |
<?php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->daily();
Scheduling Commands and Closures
You can schedule an Artisan command by name, or schedule a plain closure directly, without needing a dedicated command class at all:
<?php
// Scheduling an existing Artisan command
Schedule::command('emails:send')->daily();
// Scheduling a closure directly
Schedule::call(function () {
DB::table('recent_visits')->where('created_at', '<', now()->subMonth())->delete();
})->daily();
The Frequency API
The scheduler's biggest selling point is how readable defining "when" is. Every scheduled item returns an object with chainable frequency methods that read almost like plain English:
| Method | Runs... |
|---|---|
->everyMinute() | Every minute |
->everyFifteenMinutes() | Every 15 minutes |
->hourly() | Once per hour, on the hour |
->daily() | Once per day, at midnight |
->dailyAt('13:00') | Once per day, at the given time |
->weekly() | Once per week |
->weeklyOn(1, '8:00') | Once per week, on the given day (1 = Monday) at the given time |
->monthly() | Once per month |
Compare this to hand-writing raw cron syntax like 0 8 * * 1 — the fluent API
reads as intent ("weekly, on Monday, at 8am") rather than a cron expert having to decode
five cryptic fields.
The One Cron Entry You Actually Need
All of this PHP-defined scheduling still needs something on the server to trigger it — but instead of one crontab line per task, you need exactly one, running every minute:
* * * * * php artisan schedule:run
Every minute, schedule:run looks at everything you've defined in
routes/console.php (or the console kernel) and figures out what's actually due
to run right now, executing only that. Adding, removing, or changing a scheduled task from
then on means editing PHP code in your project — never touching the server's crontab again.
Preventing Overlapping Runs
If a scheduled task can sometimes take longer than the interval it runs on — for example a
report that's scheduled hourly but occasionally takes 70 minutes to generate — you risk two
copies of the same task running simultaneously, which can cause duplicate work or data
corruption. ->withoutOverlapping() tells the scheduler to skip a run entirely
if the previous invocation of that same task hasn't finished yet:
Schedule::command('reports:generate')->hourly()->withoutOverlapping();
Running on Only One Server
If your application is deployed across multiple servers behind a load balancer, each server
typically runs its own copy of the schedule:run cron entry — which means, without
precaution, a scheduled task could run once per server rather than once overall.
->onOneServer() tells Laravel to ensure a given task runs on only a single
server for that scheduling cycle, regardless of how many servers are checking in. This is
mostly something to simply be aware exists; it only matters once you're running more than one
application server.
Schedule::command('reports:generate')->hourly()->onOneServer();
Full Worked Example: A Weekly Digest
The Artisan Console lesson built a custom send:weekly-digest command. Wiring it
into the scheduler so it runs automatically every Monday morning, without anyone needing to
remember to run it by hand, takes one line:
<?php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('send:weekly-digest')
->weeklyOn(1, '8:00')
->withoutOverlapping();
With the server's crontab already pointed at php artisan schedule:run every
minute (as shown above), this single addition is all it takes. Every Monday at 8am,
schedule:run will notice this task is due, and will run
send:weekly-digest automatically — no manual intervention, and no separate
crontab entry to maintain.