laravel-scheduling
When to use
Use this skill when scheduling recurring tasks:
- Artisan command scheduling
- Job dispatching on a schedule
- Closure-based scheduled tasks
- Overlap prevention and maintenance mode handling
Procedure: Create a scheduled task
- Inspect existing schedule — Read
routes/console.php(Laravel 11+) orapp/Console/Kernel.phpto identify current cadence, overlap-prevention patterns, and output handling. - Pick frequency and concurrency policy — Choose a frequency helper (see table) or a cron expression; decide whether
withoutOverlapping()/onOneServer()is required. - Register the task — Add the
Schedule::command(...)/Schedule::job(...)entry; wire log destinations andrunInBackground()if needed. - Verify — Run
php artisan schedule:test(orschedule:list) and confirm timing, output path, and overlap behavior.
In routes/console.php (Laravel 11+)
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:generate')->dailyAt('06:00');
Schedule::command('telescope:prune --hours=48')->daily();
Schedule::command('queue:prune-batches --hours=48')->daily();
Schedule::job(new CleanupTemporaryFiles())->hourly();
Frequency helpers
| Method | Schedule |
|---|---|
->everyMinute() |
Every minute |
->everyFiveMinutes() |
Every 5 minutes |
->everyFifteenMinutes() |
Every 15 minutes |
->hourly() |
Every hour |
->hourlyAt(17) |
Every hour at :17 |
->daily() |
Daily at midnight |
->dailyAt('13:00') |
Daily at 1 PM |
->twiceDaily(1, 13) |
Daily at 1 AM and 1 PM |
->weekly() |
Weekly on Sunday at midnight |
->weeklyOn(1, '8:00') |
Weekly on Monday at 8 AM |
->monthly() |
Monthly on the 1st at midnight |
->monthlyOn(4, '15:00') |
Monthly on the 4th at 3 PM |
->quarterly() |
Quarterly |
->yearly() |
Yearly |
Cron expressions
Schedule::command('reports:generate')->cron('0 6 * * 1-5'); // Weekdays at 6 AM
Overlap prevention
// Prevent overlapping — skip if previous instance is still running
Schedule::command('import:customers')->hourly()->withoutOverlapping();
// With custom expiration (minutes)
Schedule::command('import:customers')->hourly()->withoutOverlapping(30);
Conditional scheduling
// Only in production
Schedule::command('reports:send')->daily()->environments(['production']);
// Custom condition
Schedule::command('sync:data')->hourly()->when(function () {
return Config::boolean('services.sync.enabled');
});
// Skip condition
Schedule::command('emails:send')->daily()->skip(function () {
return app()->isDownForMaintenance();
});
Output handling
// Log output to file
Schedule::command('reports:generate')
->daily()
->appendOutputTo(storage_path('logs/reports.log'));
// Email output on failure
Schedule::command('import:data')
->daily()
->emailOutputOnFailure('admin@example.com');
Maintenance mode
// Run even during maintenance mode
Schedule::command('queue:work')->everyMinute()->evenInMaintenanceMode();
Running the scheduler
# The scheduler itself runs every minute via system cron.
# cron has no working directory to inherit, and artisan takes no directory
# flag, so the `cd` is the shape here rather than a habit worth replacing.
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
# Test locally
php artisan schedule:work # Runs scheduler in foreground
php artisan schedule:list # Show all scheduled tasks
php artisan schedule:test # Run a specific task interactively
Grouped schedules
// routes/console.php — include schedule groups
Schedule::group('reports.php');
Schedule::group('imports.php');
// routes/console/reports.php
Schedule::command('reports:daily')->dailyAt('06:00');
Schedule::command('reports:weekly')->weeklyOn(1, '07:00');
Core rules
- Always use
withoutOverlapping()for long-running tasks. - Use
environments()to restrict production-only tasks. - Log output for debugging — use
appendOutputTo(). - Group related schedules in separate files under
routes/console/. - Monitor failures — use
emailOutputOnFailure()or Horizon for queued jobs. - One cron entry — only
schedule:rungoes in crontab, everything else in Laravel.
Output format
- Schedule definition in console kernel or schedule method
- Overlap prevention and maintenance mode configuration
Auto-trigger keywords
- schedule
- cron
- scheduled task
- recurring job
- schedule:run
- withoutOverlapping
Validate
- Verify schedule runs correctly:
php artisan schedule:listshows the task. - Confirm
withoutOverlapping()is set for long-running tasks. - Check that maintenance mode handling is configured if needed (
evenInMaintenanceMode()).
Gotcha
withoutOverlapping()uses cache locks — if the cache is cleared, overlapping can still happen.- The model forgets
runInBackground()for long-running tasks — without it, one task blocks the next. - Don't schedule tasks every minute unless absolutely necessary — it wastes server resources.
Do NOT
- Do NOT add multiple cron entries — use Laravel's scheduler for everything.
- Do NOT schedule heavy work directly — dispatch a queued job instead.
- Do NOT forget
withoutOverlapping()for tasks that may run longer than their interval. - Do NOT hardcode times — use environment-based config when schedules vary per environment.