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:

LocationUsed in
routes/console.phpNewer Laravel versions, using the Schedule facade directly
app/Console/Kernel.php, inside a schedule(Schedule $schedule) methodOlder 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:

MethodRuns...
->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.

Next up: Continue to Caching to learn how to avoid re-running expensive queries and computations on every request.