The Laravel scheduler lets you define your periodic tasks directly in your application code instead of in scattered cron lines on the server. Jobs like generating nightly reports, pruning old records, warming the cache, or pulling data from an API are planned in one central place, committed to version control, without editing crontab by hand on every server. In this article we will walk through how the scheduler works, how to define your first task, the available frequency options, and what to watch out for in production.
How the scheduler works: a single cron line
In the classic approach you write a separate cron entry for every scheduled job. Ten tasks means ten lines in the server's crontab; they live apart from your code, never show up in version control, and are painful to move to a new server. Laravel flips this around: you add just one cron entry to the server and manage everything else inside PHP.
Add this single line to the server's crontab:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
This entry runs schedule:run every minute. The command checks whether any task is due at that moment; if so it triggers them, otherwise it exits quietly. In other words, cron just provides a "heartbeat" — the logic that decides which job runs when lives entirely on the Laravel side.
Defining your first scheduled task
From Laravel 11 onward (including Laravel 12), schedule definitions live in routes/console.php via the Schedule facade. The old app/Console/Kernel.php file no longer exists. The simplest example is the built-in inspire command:
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command('inspire')->hourly();
Besides running commands, you can schedule three different kinds of work:
Schedule::command('emails:send')— runs an artisan command.Schedule::job(new HeavyReport)— pushes a queue job onto the queue.Schedule::call(fn () => DB::table('sessions')->delete())— runs a closure or callable.
To run a shell command you can also use Schedule::exec('node /scripts/import.js'). In most cases the cleanest option is to wrap the work in an artisan command and schedule that.
Writing your own command
Creating your own artisan command for a recurring job keeps the logic in one place and lets you run it by hand too. Generate the command:
php artisan make:command SendDailyReport
This creates app/Console/Commands/SendDailyReport.php. The $signature defines the command's name and handle() defines what it does:
<?php
namespace App\Console\Commands;
use App\Models\Order;
use Illuminate\Console\Command;
class SendDailyReport extends Command
{
protected $signature = 'report:daily';
protected $description = 'Builds and sends the daily sales report';
public function handle(): int
{
$total = Order::whereDate('created_at', today())->sum('total');
$this->info("Today's revenue: {$total}");
// ... email or store the report
return self::SUCCESS;
}
}
To test the command, just type php artisan report:daily in the terminal. Then add the schedule:
Schedule::command('report:daily')->dailyAt('07:30');
Frequencies and constraints
The scheduler offers a readable, fluent API. The most commonly used frequencies are:
->everyMinute(),->everyFiveMinutes(),->everyThirtyMinutes()->hourly(),->hourlyAt(15)(at minute 15 of every hour)->daily(),->dailyAt('13:00'),->twiceDaily(1, 13)->weekly(),->monthly(),->quarterly(),->yearly()
You can combine these frequencies with constraints. For example, a task that only runs on weekdays during business hours:
Schedule::command('report:daily')
->weekdays()
->between('9:00', '17:00')
->timezone('Europe/Istanbul');
To run conditionally, use ->when(fn () => Feature::active()), or its inverse ->skip(...). If the standard frequencies are not enough, you can pass a raw cron expression with ->cron('0 */6 * * *'). Since all of these methods chain, even complex schedules are expressed on a single, readable line.
Handling overlaps and multiple servers
Two important problems appear in production. The first is a task starting again before its previous run has finished. Guard a long-running task with withoutOverlapping():
Schedule::command('report:heavy')
->everyFiveMinutes()
->withoutOverlapping();
The second is running the same application on multiple servers, where every server fires the same task. If you want a task to run on only one server, add onOneServer() (this requires a Redis or database cache driver):
Schedule::command('report:daily')
->daily()
->onOneServer();
To keep one task from delaying the others, you may want to run it in the background with runInBackground(). You can also hook into the task lifecycle with before(), after(), onSuccess() and onFailure(), and route its output with emailOutputTo() or appendOutputTo().
Testing and monitoring locally
During development, instead of waiting for cron every minute, there is a command that stays running in the foreground:
php artisan schedule:work
Just like cron on the server, this triggers every minute, but it stays in your terminal — ideal for a development machine. To see all defined tasks along with their next run times:
php artisan schedule:list
If you want to run and test a specific task immediately, without waiting for its turn, php artisan schedule:test offers an interactive list. These three commands let you answer "why isn't my task running?" in seconds rather than minutes. In production, to be sure your tasks actually run, you can ping a monitoring service (such as a "dead man's switch") with thenPing().
Frequently Asked Questions
What is the difference between schedule:run and schedule:work?
schedule:run runs once: it executes the tasks that are due right now and exits; the server's cron calls it every minute. schedule:work stays open as a process and triggers itself every minute internally. Use cron + schedule:run in production, and schedule:work locally.
Should I use cron or the Laravel scheduler?
They work together; the scheduler does not replace cron, it reduces it to a single line. All your task logic stays in PHP, in version control, and testable; on the server you only manage one cron entry. This is a huge convenience, especially with multiple tasks or servers.
My scheduled task isn't running — where do I start?
First verify that the cron entry on the server points to the right directory and PHP path. Then check with php artisan schedule:list that the task is actually defined and scheduled at the right time. Most issues come from a wrong timezone or a missing or incorrect cron line.
Want to tidy up your scheduled tasks? To consolidate your scattered cron lines into a single scheduler setup or to build new periodic jobs, get in touch with me.