Task Scheduling the Centralized Way

11 Sep 2026 | Peter Cornelis

Introduction

In the past, whenever I needed a task to run on a web server, my go-to solution was a cron configuration entry.

However, as the number of scheduled tasks grows, managing cron entries across multiple servers can quickly become a burden.

Especially since you always have to connect to your server to manage your cron jobs separately from your application code.

Laravel Task Scheduling provides a centralized solution that allows you to configure your scheduling logic entirely within the application itself. Instead of defining each task separately as a cron job in the server’s configuration, you can define your scheduled tasks alongside the rest of your application code.

How does it work?

Schedules can be easily defined in your routes/console.php file.

For example, let’s say we would like to clean up all tickets that have been soft deleted for more than a month:

Schedule::call(function () {
    Ticket::onlyTrashed()->where('deleted_at', '<', now()->subMonth())->forceDelete();
})->monthly();

If you now type in the php artisan schedule:list command into your terminal you will get something like this:

0 0 * * 0  Closure at: routes/console.php:7  Next Due: 1 day from now

This allows you to see all of your scheduled tasks, and when they are due.

However, Laravel still needs to be triggered periodically to check whether a scheduled task is due. To make this work, we only need to add a single cron entry to our server.

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

This will check your scheduled tasks every minute. Laravel will then execute any tasks that are due.

If your hosting provider allows long running processes an even cleaner approach would be to use php artisan schedule:work. This command starts a continuously running process that checks and executes tasks when due. Removing the need for any cron job.

Deeper dive into Tasks

In our initial example, we have already seen how to define closures in routes/console.php as a way of implementing scheduled tasks. However there are other possibilities, let’s take a deeper look.

Invokable Objects

Instead of defining the logic directly inside a closure, we can move it into an invokable class. This keeps our routes/console.php file clean.

Let’s implement this on our previous example:

<?php
namespace App\Tasks;

use App\Models\Ticket;

class CleanupTickets
{
public function __invoke()
{
Ticket::onlyTrashed()
->where('deleted_at', '<', now()
->subMonth())
->forceDelete();
}
}

Now, in our routes/console.php file we only need to add:

Schedule::call(new CleanupTickets)->monthly();

Artisan Commands

Additionally, you can also schedule Artisan commands.

Let’s say, for example, we want to soft delete all tickets that are stale for longer than 3 months.

First, let's create our command:

php artisan make:command CleanupStaleTickets

The command would look as follows:

<?php

namespace App\Console\Commands;

use ...

#[Signature('app:cleanup-stale-tickets')]
#[Description('Cleans up stale tickets older than 3 months')]
class CleanupStaleTickets extends Command
{
public function handle(): void
{
Ticket::query()
->where('updated_at', '<', now()->subMonths(3))
->delete();
}
}

In our routes/console.php file we only need to add:

Schedule::command('app:cleanup-stale-tickets')->weekly();

Queued Jobs

Laravel also allows you to schedule queued jobs directly using Schedule::job().

For example, let's say we want to send reminders for tickets that have been stale for too long:

Schedule::job(new SendStaleTicketReminders)->daily();

The Job contains the actual work:

<?php

namespace App\Jobs;

use ...

class SendStaleTicketReminders implements ShouldQueue
{
use Queueable;

public function handle(): void
{
Ticket::query()
->where('status', 'open')
->where('updated_at', '<', now()->subMonths(2))
->with('user')
->get()
->each(function ($ticket) {
$ticket->update(['status' => 'stale']);
Mail::to($ticket->user->email)
->send(new App\Mail\StaleTicketReminder($ticket));
});
}
}

Here we check all the tickets that have an open status and haven't beenupdated for two months. Once the scheduled task is due, the SendStaleTicketReminders job is dispatched to the queue. There a queue worker can pick up the job.

Difference between the Scheduler, Jobs and Queues

At this point, we have seen that Laravel's Scheduler can determine when a task should run. And we have also seen that a scheduled task can dispatch a queued job.

This leaves us with the question: where does the task end and the job begins? The easiest way to understand this is to look at the responsibility of each component.

The scheduler is responsible for determining when something should happen. It checks the schedule and executes the scheduled task once it is due. When the task is a queued job, Laravel dispatches the job to the queue.

With a queued job your application can accept the work without having to process everything immediately. The jobs are then stored in a queue until a worker can execute it. For this to work there has to be a queue worker available.


Peter Cornelis

Peter Cornelis

Software Engineer