Laravel Scheduler with Cron Job
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
Mastering Laravel Scheduling: Why Your Cron Jobs Seem to Be Skipping Execution
As senior developers, we often deal with the complexities of scheduling tasks—whether it’s managing background jobs, sending periodic emails, or executing database maintenance. When integrating Laravel's powerful scheduler with external system tools like Cron, subtle timing issues can derail your operations.
The scenario you described—setting a command to run every minute and observing inconsistent execution when using php artisan schedule:run—is a very common point of confusion. This post will dive deep into why this happens and how to set up robust, reliable scheduling in a Laravel application.
The Misconception: Artisan Scheduler vs. System Cron
The core issue often lies in misunderstanding the roles of two separate scheduling mechanisms: Laravel's internal scheduler and the operating system’s native cron service.
Laravel’s scheduler (managed via php artisan schedule:run) is designed to be executed on demand from the command line. It checks the scheduled times only when you explicitly run that command. It is not a continuous, self-executing loop running in the background like a traditional cron job.
When you manually run php artisan schedule:run, it executes all jobs that are due at that exact moment. If you wait two minutes and no log appears, it simply means that the system clock has advanced past the next scheduled execution time, and you haven't explicitly told the system to check again.
The Correct Approach: Leveraging System Cron for Reliability
For tasks that need to run reliably at fixed intervals (like every minute), the most robust solution is to delegate the scheduling responsibility entirely to your operating system’s native cron scheduler. This decouples the execution from the Laravel application itself, making it far more resilient and reliable, especially in production environments.
Step 1: Prepare Your Command for Cron Execution
Instead of relying on manually running schedule:run, you should configure the server's crontab file to execute the Artisan command directly every minute. This ensures that the Laravel environment is invoked by the OS scheduler, not just a single user session.
If your schedule needs to run every minute, your crontab entry would look something like this (assuming PHP 5 context):
* * * * * /usr/bin/php /path/to/your/artisan schedule:run >> /var/log/laravel_schedule.log 2>&1
This command tells the operating system to execute the Artisan scheduler every minute, redirecting all output (both standard output and errors) into a dedicated log file. This is crucial for debugging, as seen in best practices for deployment and monitoring on platforms like laravelcompany.com.
Step 2: Ensuring Job Execution within the Command
Your command itself must be structured to handle its execution cleanly. When running via cron, you need to ensure that Laravel is executing the command correctly, which often involves setting up the environment variables properly—something that Laravel handles internally when invoked this way.
If your actual task is just running a specific command (like your RemoveUnpaidInvoice class), you can directly call it within the cron job definition instead of relying solely on the scheduler running the entire queue:
* * * * * /usr/bin/php /path/to/artisan removeUnpaidInvoice >> /var/log/invoice_job.log 2>&1
This method bypasses the complexity of managing the schedule file for simple, high-frequency tasks and directly invokes the necessary business logic on a fixed timeline managed by the operating system.
Code Review and Best Practices
Your initial setup with the RemoveUnpaidInvoice class is perfectly fine for defining what needs to be done:
// app/Console/Commands/RemoveUnpaidInvoice.php
public function handle()
{
// Ensure this logic is atomic and handles potential database lock issues
UserLog::create([
'user_id' => '3',
'title' => 'Cron Testing',
'log' => 'Time : ' . date('H:i:s')
]);
}
The key takeaway is that complex scheduling should be handled by dedicated system tools (like Cron), while Laravel’s scheduler is best used for less frequent, more complex, or human-initiated tasks. For high-frequency monitoring, relying on the OS cron ensures true, continuous execution regardless of when a developer decides to manually run an Artisan command.
Conclusion
The inconsistency you observed was not a mistake in your code, but rather a gap between how Laravel’s internal scheduler works and how external system timers operate. To achieve reliable, minute-by-minute scheduling: delegate the timing to Cron. By configuring the operating system's cron job to execute php artisan schedule:run every minute, you establish a robust, continuous execution loop that is independent of manual intervention, ensuring your automated invoice removal process runs exactly when it is supposed to.