Laravel

Scheduled Tasks in Laravel

How to define and manage scheduled tasks in Laravel using the task scheduler, including cron setup, common schedule frequencies, and best practices.

Laravel's task scheduler lets you define all of your scheduled jobs in PHP, avoiding the need to manage multiple cron entries. A single cron entry on the server runs the scheduler every minute, and Laravel decides which tasks are due.

Setting Up the Cron Entry

Add a single entry to your server's crontab:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

That's the only server-level configuration needed. All scheduling logic lives in your application code.

Defining Schedules

Define your schedule in routes/console.php or in a service provider using the Schedule facade:

use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:daily')->dailyAt('06:00');
Schedule::command('cache:prune')->hourly();
Schedule::job(new ProcessPendingPayments)->everyFiveMinutes();

Frequency Options

Laravel provides a wide range of frequency helpers:

->everyMinute()
->everyFiveMinutes()
->everyTenMinutes()
->everyThirtyMinutes()
->hourly()
->hourlyAt(17)          // At 17 minutes past each hour
->daily()
->dailyAt('13:00')
->twiceDaily(1, 13)     // At 01:00 and 13:00
->weekly()
->weeklyOn(1, '8:00')   // Monday at 8am
->monthly()
->monthlyOn(4, '15:00') // 4th of each month at 3pm
->quarterly()
->yearly()
->cron('0 */3 * * *')   // Custom cron expression

Preventing Overlaps

By default, a scheduled task will run even if the previous instance is still running. Use withoutOverlapping() to prevent this:

Schedule::command('reports:generate')
    ->daily()
    ->withoutOverlapping();

This acquires a cache lock before running. If the lock is already held, the task is skipped.

Running Tasks on One Server

In a multi-server setup, every server will execute the scheduled tasks. Use onOneServer() to ensure a task runs only on a single server:

Schedule::command('db:backup')
    ->daily()
    ->onOneServer();

This requires a shared cache driver (Redis or Memcached). The first server to acquire the lock runs the task; the others skip it.

Running Tasks in the Background

By default, scheduled tasks run in the foreground, blocking subsequent tasks. Use runInBackground() to allow concurrent execution:

Schedule::command('reports:weekly')
    ->weekly()
    ->runInBackground();

Handling Output

Redirect command output to a log file for debugging:

Schedule::command('reports:daily')
    ->daily()
    ->sendOutputTo(storage_path('logs/reports.log'));

// Append instead of overwrite
->appendOutputTo(storage_path('logs/reports.log'));

// Email output when the command produces any output
->emailOutputTo('[email protected]');

Running the Scheduler Locally

During development, run the scheduler in the foreground to watch it execute tasks as they become due:

php artisan schedule:work

Testing Scheduled Tasks

Use Schedule::fake() to assert that tasks were scheduled without actually running them:

use Illuminate\Support\Facades\Schedule;

Schedule::fake();

// Boot your application...

Schedule::assertRanOn('app:send-reminders', now()->subMinutes(5));

To test the actual command logic, call it directly in a feature test:

php artisan test --filter=DailyReportCommandTest

Tips

  • Keep individual scheduled commands short and focused. Long-running tasks belong in a queue job dispatched by the scheduled command.
  • Log the start and end of each scheduled task so you can detect silent failures.
  • Use withoutOverlapping() for any task that modifies shared state.
  • In production, monitor that schedule:run is actually being invoked. A misconfigured cron entry is a common silent failure. Laravel Pulse or an uptime monitor can alert you if key scheduled tasks stop running.
← Older
Secrets and Config Validation in Laravel
Newer →
Route Organisation and Naming in Laravel

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.