To run scheduled tasks in Laravel 13, define them in routes/console.php with the Schedule facade and add one cron entry that runs php artisan schedule:run every minute. Laravel then decides which tasks are due. This guide targets Laravel 13 and PHP 8.3 or newer.
What do you need before you start?
- A Laravel 13 application on PHP 8.3 or newer.
- Shell access to the server so you can edit the crontab (or a Windows Task Scheduler job on a local machine).
- Basic familiarity with Artisan commands.
Step 1: Create a command to schedule
Any Artisan command can be scheduled. Create a small one that removes expired password reset tokens from a custom table, or any cleanup job of your own.
php artisan make:command PruneStaleExports<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class PruneStaleExports extends Command
{
protected $signature = 'exports:prune {--days=7}';
protected $description = 'Delete export records older than the given number of days';
public function handle(): int
{
$deleted = DB::table('exports')
->where('created_at', '<', now()->subDays((int) $this->option('days')))
->delete();
$this->info("Deleted {$deleted} stale exports.");
return self::SUCCESS;
}
}Step 2: How do you register the schedule?
Since Laravel 11 there is no app/Console/Kernel.php. You declare the schedule in routes/console.php.
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command('exports:prune --days=7')
->dailyAt('02:30')
->timezone('Asia/Karachi')
->withoutOverlapping()
->onOneServer();
Schedule::call(function () {
cache()->forget('homepage.stats');
})->hourly();
Schedule::job(new \App\Jobs\SendDigestEmails)->weekdays()->at('08:00');You can schedule an Artisan command, a closure, a queued job or a shell command (Schedule::exec()). Frequency helpers such as everyMinute(), hourly(), dailyAt(), weeklyOn() and cron('* * * * *') cover most needs.
Step 3: Add the single cron entry
On Linux, open the crontab of the user that owns the project and add this line (change the path):
* * * * * cd /var/www/my-app && php artisan schedule:run >> /dev/null 2>&1This is the only cron line you will ever need. When you add new tasks, you edit PHP code, not the server.
Step 4: How do you run the scheduler while developing?
Locally you do not need cron. Run the worker command in a terminal and leave it open:
php artisan schedule:workIt calls the scheduler every minute in the foreground. To see what is registered, run:
php artisan schedule:listWhich options prevent common scheduling problems?
- withoutOverlapping() stops a new run starting while the previous one is still working. It uses a cache lock, so your cache driver must support locks.
- onOneServer() makes sure only one server runs the task when you deploy several servers. It needs a shared cache store such as Redis or the database, not the array or file store on separate machines.
- runInBackground() lets long commands run without blocking the other tasks due in the same minute.
- when() and skip() let you add conditions, for example run only in production.
- emailOutputOnFailure(), appendOutputTo() and the
onFailure()hook help with monitoring.
Schedule::command('reports:build')
->hourly()
->when(fn () => app()->isProduction())
->appendOutputTo(storage_path('logs/reports-schedule.log'))
->onFailure(function () {
logger()->error('reports:build failed');
});Common mistakes
- Putting the schedule in the cron file as many lines. Use one
schedule:runline only. - Running cron as a different user than the one that owns
storage/. Log and cache files then become unwritable. - Forgetting the timezone. Without
timezone()or theschedule_timezonevalue inconfig/app.php, times follow the application timezone. - Using
onOneServer()with a per-machine cache store. - Expecting tasks to run on a machine where the cron job is missing after a deployment or container rebuild.
How do you verify that it works?
- Run
php artisan schedule:listand confirm your task and its next due time appear. - Run
php artisan exports:prune --days=0manually to check the command itself. - Temporarily change a task to
everyMinute()withappendOutputTo(), wait a minute and read the log file. - Run
php artisan schedule:testto pick and run a single task on demand.
How do task groups and maintenance mode behave?
When several tasks share the same settings, group them so you do not repeat yourself. Laravel lets you wrap them in a closure with shared constraints.
Schedule::daily()
->onOneServer()
->timezone('Asia/Karachi')
->group(function () {
Schedule::command('exports:prune');
Schedule::command('reports:build');
});By default, scheduled tasks do not run while the application is in maintenance mode (php artisan down). If a task must still run, add evenInMaintenanceMode(). This matters for backups and health checks.
How do you test a scheduled command?
Test the command, not the clock. Call it with Artisan in a feature test and assert on the result. This keeps the schedule definition thin and the logic testable.
it('prunes old exports', function () {
DB::table('exports')->insert(['created_at' => now()->subDays(10)]);
$this->artisan('exports:prune --days=7')
->expectsOutput('Deleted 1 stale exports.')
->assertExitCode(0);
});FAQ
Do I need a queue worker for scheduled tasks?
No, unless the scheduled item is a queued job. A command or closure runs inside the schedule:run process. A job scheduled with Schedule::job() is pushed to the queue and needs a worker.
How often should schedule:run be called?
Every minute. Laravel checks which tasks are due and ignores the rest, so a minute is the finest resolution you get.
How do I run the scheduler in Docker?
Run php artisan schedule:work as a separate long-running container or process, so you do not depend on cron inside the image.
Can I schedule tasks every few seconds?
Yes. Laravel supports sub-minute tasks such as everyFiveSeconds(), but then schedule:run keeps running until the end of the minute, so plan for it when deploying.



