Programming Beginner 8 min

How to Schedule Recurring Tasks in Laravel

Laravel's task scheduler lets you define all your cron jobs in PHP — no need to edit the server's crontab for each new task. You register one cron entry that calls php artisan schedule:run every minute, and Laravel handles the rest.

This guide covers creating a custom Artisan command, registering it in the scheduler, setting frequencies, preventing overlapping runs, and logging output — all using the Laravel 11+ approach (with a note on the older Kernel-based method).

Step-by-step

  1. 1

    Create an Artisan command

    Generate a new Artisan command with make:command. Give it a descriptive name. The command's $signature becomes the string you use to call it from the scheduler and the terminal.

    bash
    php artisan make:command SendDailyReport
  2. 2

    Write the command logic

    Open app/Console/Commands/SendDailyReport.php. Set the $signature and $description, then write your task logic in the handle() method. Return Command::SUCCESS at the end so the scheduler knows the command completed without errors.

    php
    <?php
    
    namespace App\Console\Commands;
    
    use Illuminate\Console\Command;
    
    class SendDailyReport extends Command
    {
        protected $signature = 'report:daily';
        protected $description = 'Send the daily summary report to admins';
    
        public function handle(): int
        {
            // Your task logic here
            $this->info('Generating daily report...');
    
            // e.g. Mail::to('admin@example.com')->send(new DailyReport());
    
            $this->info('Report sent successfully.');
    
            return Command::SUCCESS;
        }
    }
  3. 3

    Register the schedule in routes/console.php (Laravel 11+)

    In Laravel 11 and newer, scheduled tasks are defined in routes/console.php using the Schedule facade. This file is automatically loaded — no service provider registration needed.

    In Laravel 10 and older, you define the schedule in the schedule() method of app/Console/Kernel.php instead. The frequency methods are identical in both versions.

    php
    <?php
    // routes/console.php (Laravel 11+)
    
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::command('report:daily')->dailyAt('07:00');
    
    // Laravel 10 — app/Console/Kernel.php
    // protected function schedule(Schedule $schedule): void
    // {
    //     $schedule->command('report:daily')->dailyAt('07:00');
    // }
  4. 4

    Use the right frequency method

    Laravel ships with fluent frequency helpers for almost every interval. A few of the most useful ones are shown below. Chain multiple constraints to compose precise schedules — for example ->weekdays()->hourly().

    php
    Schedule::command('report:daily')->daily();           // midnight
    Schedule::command('report:daily')->dailyAt('08:00');   // 08:00 every day
    Schedule::command('sync:data')->everyFiveMinutes();     // every 5 min
    Schedule::command('sync:data')->everyFifteenMinutes();  // every 15 min
    Schedule::command('cleanup')->weekly()->sundays()->at('03:00');
    Schedule::command('newsletter')->monthlyOn(1, '09:00'); // 1st of month
    Schedule::command('ping')->everyMinute();               // every minute
    Schedule::command('report')->weekdays()->at('09:00');   // Mon–Fri at 9am
  5. 5

    Prevent overlapping runs

    By default, if a scheduled command is still running when the next execution fires, both instances run simultaneously. Add ->withoutOverlapping() to ensure only one instance runs at a time. Laravel uses an atomic lock to enforce this.

    php
    Schedule::command('sync:orders')
        ->everyFiveMinutes()
        ->withoutOverlapping()  // skip if previous run is still active
        ->runInBackground();    // run the command without blocking the scheduler process
  6. 6

    Log command output to a file

    Use ->appendOutputTo() to capture the command's output (everything printed via $this->info() or echoed) to a log file. This is invaluable for debugging cron failures silently swallowed by the scheduler.

    php
    Schedule::command('report:daily')
        ->daily()
        ->appendOutputTo(storage_path('logs/daily-report.log'));
  7. 7

    Add the single cron entry on the server

    On your production server, add exactly one cron entry — it runs schedule:run every minute. Laravel reads all your scheduled tasks from that single command and decides which ones to fire based on the current time.

    Open the crontab with crontab -e and add the line below. Replace the path with your actual project path and the PHP binary path (find it with which php).

    bash
    * * * * * cd /var/www/myapp.com && php artisan schedule:run >> /dev/null 2>&1
  8. 8

    Test the schedule locally

    You don't need to wait for cron to test your tasks. Use schedule:run manually to fire all tasks due right now, or schedule:work to run the scheduler in the foreground every minute (useful during development).

    bash
    # Trigger all due tasks once (same as what cron does)
    php artisan schedule:run
    
    # Run scheduler in foreground, polling every minute
    php artisan schedule:work
    
    # List all registered tasks and their next run time
    php artisan schedule:list

Tips & gotchas

  • Always use `->withoutOverlapping()` on any task that touches external APIs or runs database-heavy operations — duplicate runs cause subtle data corruption that's hard to debug.
  • Run `php artisan schedule:list` to verify the next expected run time for each task before deploying to production.
  • Set the cron entry to run as the web server user (e.g., `www-data`) not root, so file permissions created during the task match what the app expects.
  • Use `->onSuccess()` and `->onFailure()` callbacks to send a notification or write to a log when a scheduled task completes or fails.

Wrapping up

You now have a fully working task scheduler — a single cron entry drives all your scheduled commands, with overlap protection and log output. From here, explore ->sendOutputTo() combined with a Slack or email notification on failure, or look at running closures and anonymous jobs directly in the scheduler for lightweight one-off tasks.

#Laravel #Cron #Scheduler
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.