Mastering Scheduled Tasks in Laravel

Keep scheduled tasks running reliably. Learn how to prevent delays and overlaps, set execution windows, capture output, and monitor failures.

All articles

What you’ll learn

  • Prevent task delays, overlaps, and duplicate work.
  • Control execution windows and capture command output.
  • Monitor failures, skipped executions, and delays.

Laravel scheduled tasks are a powerful feature in Laravel. They let you run background tasks on a schedule without the need for setting up a separate cron entry for each one. But if you aren't careful, they may silently fail or result in performance bottlenecks. In this post, I will show you how to manage your scheduled tasks efficiently.

Avoid silent delays in scheduled tasks

Let’s assume you have two scheduled tasks in your routes/console.php file as below.

PHP
Schedule::command('task:a')
    ->everyMinute();

Schedule::command('task:b')
    ->everyMinute();

You would assume these two scheduled tasks run every minute without any delays. But that's not the case and let me explain why.

PHP
class TaskA extends Command
{
    protected $signature = 'task:a';

    public function handle()
    {
        Log::info('log from task A');
        sleep(10);
    }
}
PHP
class TaskB extends Command
{
    protected $signature = 'task:b';

    public function handle()
    {
        Log::info('log from task B');
    }
}

Now, when you run php artisan schedule:run and inspect your log file, You will see below.

Log output
[2026-02-10 04:43:00] local.INFO: log from task A
[2026-02-10 04:43:10] local.INFO: log from task B

Noticed how log entry from TaskB is delayed by 10 seconds? That's how long previous scheduled task(TaskA) took to run. If TaskA would take 5 minutes to complete, Then your TaskB will be delayed for 5 minutes.

Run tasks in the background

To avoid delays, You simply chain runInBackground() to your scheduled tasks.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->runInBackground();

Schedule::command('task:b')
    ->everyMinute()
    ->runInBackground();

This will run both of your scheduled tasks in parallel in a separate PHP process.

Prevent overlaps and duplicate work

Let’s assume we have a scheduled task TaskA scheduled to run every minute like below.

PHP
Schedule::command('task:a')
    ->everyMinute();
PHP
class TaskA extends Command
{
    protected $signature = 'task:a';

    public function handle()
    {
        $emails = DB::table('emails')->whereNull('sent_at')->get();
        foreach ($emails as $email) {
            $this->sendEmail($email);
            DB::table('emails')->where('id', $email->id)
                ->update(['sent_at' => now()]);
        }
    }
}

If this task runs longer than one minute, Then next schedule will kick in and start running it again. This may result in sending duplicate emails. To avoid, chain the withoutOverlapping() method.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->withoutOverlapping();

By default, withoutOverlapping() uses a lock that expires after 24 hours and is released when the task finishes. Pass the expiry in minutes: withoutOverlapping(5) sets a five-minute expiry. Choose an expiry longer than the task’s expected runtime so the lock cannot expire while it is still running.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->withoutOverlapping(5);

Run only on one server

Use onOneServer() when the scheduler runs on several servers. It selects one server for each scheduled execution. All servers must share a central cache using a supported cache driver, such as Redis or the database driver. Keep withoutOverlapping() to prevent the next execution starting before the previous one finishes.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->withoutOverlapping()
    ->onOneServer();

Run scheduled tasks conditionally

Sometimes, you may not want to run a scheduled task for various reasons. You can define custom logic to tell Laravel whether a schedule task should run or not. Laravel checks the logic before each execution. So you can safely rely on it to skip only certain times where you do not want it to run.

In the above example where you are sending emails, You do not want to run the scheduled task if you hit a rate limit. You can do this by chaining the skip method as below.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->skip(fn () => (new EmailService())->isRateLimited());

Run only on specific environments

PHP
class TaskA extends Command
{
    protected $signature = 'task:a';

    public function handle()
    {
        DB::table('emails')
            ->where('sent_at', '<=', now()->subDays(60))
            ->delete();
    }
}

Consider the above scheduled task where you are deleting emails which are older than 60 days to save storage costs. But you only want to delete these emails in your non production environments without affecting the production data. In this case, You can chain the environments() method to instruct Laravel which environments this scheduled task can run on.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->environments(['local', 'testing', 'development', 'staging']);

Run during business hours

If you wish to run a scheduled task at a specific time, Lets say during your business hours, You can do that as shown below.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->weekdays()
    ->timezone('America/New_York')
    ->between('9:00', '17:00');

Use unlessBetween() to exclude lunch hours. Update holiday dates each year and review daylight-saving transitions, which can cause a timezone-based task to run twice or be skipped.

PHP
Schedule::command('task:a')
    ->everyMinute()
    ->weekdays()
    ->timezone('America/New_York')
    ->between('9:00', '17:00')
    ->unlessBetween('12:00', '13:00') // Skip lunch
    ->skip(function () { // Exclude holidays
        $today = now('America/New_York');
        $holidays = [
            // month => [days]
            1 => [
                1, // New Year
            ],
            7 => [
                4, // US Independence Day
            ],
            11 => [
                26, // Thanksgiving in 2026; update each year
            ],
            12 => [
                24, // Christmas Eve,
                25, // Christmas,
                31, // New year Eve
            ],
        ];

        if (in_array($today->month, array_keys($holidays)) ) {
            return in_array($today->day, $holidays[$today->month]);
        }

        return false;
    });

Spread out resource-intensive tasks

Some scheduled tasks are resource intensive and some are not. But when you are running multiple scheduled tasks at the same time parallel, Each of them will put strain on your server and database resources. Lets look at the below use case.

PHP
Schedule::command('task:a')
    ->runInBackground()
    ->everyFiveMinutes();

Schedule::command('task:b')
    ->runInBackground()
    ->everyFiveMinutes();

Schedule::command('task:c')
    ->runInBackground()
    ->everyFiveMinutes();

Schedule::command('task:d')
    ->runInBackground()
    ->everyFiveMinutes();

Schedule::command('task:e')
    ->runInBackground()
    ->everyFiveMinutes();

In the example above, I have five scheduled tasks running every five minutes. This will put strain on my server and database. But i cannot afford to delay them. They must run at least once every five minutes.

In this case, I can have them run each task at a different minute within the five minute interval, so each of them still runs at least once every five minutes.

PHP
Schedule::command('task:a')
    ->runInBackground()
    ->cron('0/5 * * * *');

Schedule::command('task:b')
    ->runInBackground()
    ->cron('1/5 * * * *');

Schedule::command('task:c')
    ->runInBackground()
    ->cron('2/5 * * * *');

Schedule::command('task:d')
    ->runInBackground()
    ->cron('3/5 * * * *');

Schedule::command('task:e')
    ->runInBackground()
    ->cron('4/5 * * * *');

Capture scheduled task output

Most of your scheduled tasks are artisan commands which will produce some output. You can see this output in terminal when running the commands manually. But when they were ran as part of the scheduled tasks, There are certain ways where you can still capture and view the output.

PHP
// Send output to email even if there is no output produced.
Schedule::command('task:a')
    ->everyFiveMinutes()
    ->emailOutputTo('[email protected]');

// Send output to email only if there is output produced.
Schedule::command('task:a')
    ->everyFiveMinutes()
    ->emailWrittenOutputTo('[email protected]');

// Send output to email only if the task failed.
Schedule::command('task:a')
    ->everyFiveMinutes()
    ->emailOutputOnFailure('[email protected]');

// Store output in a file. This will replace the content of the file if it already exists.
Schedule::command('task:a')
    ->everyFiveMinutes()
    ->sendOutputTo(storage_path('logs/task-a-output.log'));

// Store output in a file. This will append output to the file if it already exists.
Schedule::command('task:a')
    ->everyFiveMinutes()
    ->appendOutputTo(storage_path('logs/task-a-output.log'));

Use before and after hooks

In certain cases, You would like to execute custom logic before and after a scheduled task has been executed.

In the below example, We are ensuring the image directory exists before generating images and cleaning up raw images after generation.

PHP
Schedule::command('generate:images')
    ->everyFiveMinutes()
    ->before(function () {
        // Make sure the image generation directory exists
        if (! Storage::directoryExists('images/generated')) {
            Storage::makeDirectory('images/generated');
        }
    })
    ->after(function () {
        // Clean up raw files after generation
        Storage::deleteDirectory('images/raw');
        Storage::makeDirectory('images/raw');
    });

You can also ping specific urls like monitoring services to inform them the start and completion of your scheduled tasks.

PHP
// Ping before and after execution
Schedule::command('generate:images')
    ->everyFiveMinutes()
    ->pingBefore('https://monitor.example.com/before-execution')
    ->thenPing('https://monitor.example.com/after-execution');

// Only ping in production environment
Schedule::command('generate:images')
    ->everyFiveMinutes()
    ->pingBeforeIf(
        app()->environment('production'),
        'https://monitor.example.com/before-execution'
    )
    ->thenPingIf(
        app()->environment('production'),
        'https://monitor.example.com/after-execution'
    );

// Ping a separate url for success and failures
Schedule::command('generate:images')
    ->everyFiveMinutes()
    ->pingBefore('https://monitor.example.com/before-execution')
    ->pingOnFailure('https://monitor.example.com/failed-execution')
    ->pingOnSuccess('https://monitor.example.com/success-execution');

Handle maintenance mode and system users

By default, Your scheduled tasks wont be executed if your application is under maintenance mode. If you wish to run them even in maintenance mode, chain evenInMaintenanceMode() method.

PHP
Schedule::command('generate:images')
    ->everyFiveMinutes()
    ->evenInMaintenanceMode();

By default, All your scheduled tasks will be ran by the same system user who is running the task scheduler. If you wish to change the system user which it runs as, You can chain the user() method.

PHP
Schedule::command('generate:images')
    ->everyFiveMinutes()
    ->user('www-data');

Monitor failures, skips, and delays

Scheduled tasks are crucial for any business which relies on them. Delays, failures and overlaps can cause serious problems if there is no proper visibility into the execution of your scheduled tasks.

That's where Laritor comes in. Laritor is a performance monitoring and issue tracking tool for Laravel applications. It lets you monitor your Laravel scheduled tasks for failures, delays and skipped executions and instantly alerts you when problems are detected. Laritor captures your entire scheduled task output so you can view the output of each task which will help you debug further. Explore scheduled task monitoring in Laritor.

Scheduled tasks are a very powerful feature in Laravel. If you enjoyed this article, Share this with your network to spread the knowledge.

Further reading

Official documentation

Back to the blogBack to top

Start optimizing your Laravel app

Know when your scheduled tasks need attention.

Monitor failures, delays, and skipped executions in your Laravel app. Open a recorded task to inspect its output and related events.

Try for free

300K events free. Choose what you record.