Task Scheduling with Cron in Laravel

An invoice reminder that should go out at 8 a.m. never arrives, and nobody notices until a customer complains. Or the daily report lands twice because two runs overlapped. These pr...

Task Scheduling with Cron in Laravel

An invoice reminder that should go out at 8 a.m. never arrives, and nobody notices until a customer complains. Or the daily report lands twice because two runs overlapped. These problems almost always come from scheduled work that is neither organized nor monitored.

Laravel's task scheduler moves every schedule into your codebase and needs just one cron entry on the server. This guide covers the scheduler in Laravel 11: how it works, writing scheduled commands, time zones, overlap protection, logging, server setup and debugging schedules that do not run.

1. How the Scheduler Works

Cron calls php artisan schedule:run every minute. That command reads your schedule definitions, works out which are due this minute and runs them.

AspectMany manual cron entriesLaravel scheduler
Where schedules liveEach server's crontabroutes/console.php in Git
Preventing double runsWrite your own lockswithoutOverlapping()
Per-task time zoneServer time onlytimezone() per task
Logs and failure alertsManual output redirectsappendOutputTo(), onFailure()
Listing schedulescrontab -lphp artisan schedule:list

2. A Command for Recurring Work

Closures can be scheduled, but artisan commands are easier to test and run by hand.

php artisan make:command SendInvoiceReminders
class SendInvoiceReminders extends Command
{
    protected $signature = 'invoices:remind {--days=3 : Days before the due date}';
    protected $description = 'Send reminders for invoices that are about to fall due';

    public function handle(): int
    {
        $target = today()->addDays((int) $this->option('days'));
        $count = 0;

        Invoice::where('status', 'unpaid')
            ->whereDate('due_date', $target)
            ->whereNull('reminded_at')
            ->with('customer')
            ->chunkById(200, function ($invoices) use (&$count) {
                foreach ($invoices as $invoice) {
                    $invoice->customer->notify(new InvoiceDueSoon($invoice));
                    $invoice->update(['reminded_at' => now()]);
                    $count++;
                }
            });

        $this->info("Reminders sent: {$count}");

        return self::SUCCESS;
    }
}

The reminded_at column makes the command idempotent: if it accidentally runs twice, nobody is emailed twice. Any scheduled task that sends messages or touches money should work this way.

3. Registering Schedules

New Laravel 11 projects have no console kernel. Schedules go in routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('invoices:remind --days=3')
    ->dailyAt('08:00')
    ->timezone('America/New_York')
    ->withoutOverlapping()
    ->appendOutputTo(storage_path('logs/schedule-invoices.log'));

Schedule::command('reports:daily')
    ->weekdays()
    ->at('07:00')
    ->onFailure(fn () => logger()->error('reports:daily failed'));

Schedule::command('model:prune')->daily();
Schedule::command('queue:prune-failed --hours=168')->weekly();

Handy Frequencies

->everyFiveMinutes();
->hourlyAt(15);
->weekdays()->at('09:00');
->weeklyOn(1, '06:00');      // Monday 06:00
->monthlyOn(1, '00:30');
->cron('0 */2 * * *');
->environments(['production']);

4. Time Zones

Servers usually run on UTC. A task written as dailyAt('08:00') in a UTC app runs at 8 a.m. UTC, which may be the middle of the night for your customers. Either set APP_TIMEZONE for the whole app, or keep the app on UTC and add ->timezone() per task. The second option is safer when you serve several regions.

5. Installing the Cron Entry

  1. VPS: run crontab -e as the user that owns the project and add the line below.
  2. Control panels such as cPanel or Plesk: add a cron job set to every minute with the full PHP path.
  3. Make sure the CLI PHP version matches the web PHP version.
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Locally, skip cron and run php artisan schedule:work.

6. Overlaps and Slow Tasks

  • withoutOverlapping() skips a run if the previous one is still going. The lock expires after 24 hours by default; pass minutes, such as withoutOverlapping(30), to shorten it.
  • runInBackground() stops a slow task from delaying others due the same minute.
  • onOneServer() runs a task once across several servers, using a shared cache store.
  • Queue heavy work: let the command dispatch jobs and let workers do the lifting.

On hosts that do not allow a permanent worker process, the scheduler can drain the queue:

Schedule::command('queue:work --stop-when-empty --max-time=50')
    ->everyMinute()
    ->withoutOverlapping();

7. Debugging a Schedule That Does Not Run

  1. php artisan schedule:list shows each task and its next run. Missing task means a problem in routes/console.php.
  2. php artisan schedule:test runs one schedule on demand.
  3. Run the command directly to see its error output.
  4. Temporarily send cron output to a log file instead of /dev/null to confirm cron calls Laravel at all.
  5. Clear stuck overlap locks with php artisan schedule:clear-cache.

8. Common Mistakes

  • Tasks run at the wrong hour: server and app time zones differ.
  • "Could not open input file: artisan": cron is not in the project folder. Add cd /path &&.
  • Log files cannot be written: cron runs as a different user from the web server.
  • Duplicate emails: the command is not idempotent.

Production Checklist

  • One cron entry per server, using the correct PHP binary.
  • Every task has an explicit time zone.
  • Tasks that send messages or change data are idempotent.
  • Long tasks use withoutOverlapping() or a queue.
  • Output is logged and failures trigger an alert.
  • schedule:list is checked after each deploy.

For long-running work, read Laravel queues and jobs, and for setting up cron on a budget host, see deploying Laravel to shared hosting.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Task Scheduling Laravel Programming & Laravel
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now