How-To: Schedule a Recurring Task
Wire a console command into the scheduler so it runs automatically.
The scheduler lives in routes/console.php (not the legacy app/Console/Kernel.php).
1. Register the schedule
php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:reservations:mark-no-shows')
->dailyAt('00:05')
->withoutOverlapping()
->onOneServer();2. Use the safety guards by default
For any scheduled command, add these three:
| Guard | Why |
|---|---|
->withoutOverlapping() | Prevents two runs from stacking if the previous one took longer than the interval |
->onOneServer() | In multi-server deployments, only one server runs the job (requires Redis cache) |
->runInBackground() (optional) | Frees the scheduler tick for long-running jobs |
The only schedules in CPR that don't use these guards are very fast cleanup tasks (app:prune-activity-logs).
3. Cadence patterns in use
php
->daily() // 00:00
->dailyAt('03:00') // pick an off-peak hour
->hourly()
->everyFiveMinutes()
->weeklyOn(0, '01:00') // Sunday 01:00
->cron('*/15 * * * *') // raw cron when you need itCPR conventions:
- Backups:
dailyAt('03:00')—cpr:backup:database - Pruning:
dailyAt('02:30')—app:prune-audit-log - Daily roll-overs (no-show, etc.):
dailyAt('00:05')— just after midnight, so the previous day is locked in - Avoid
dailyAt('00:00')— many other systems run then
4. Verify the schedule
bash
# List every scheduled task with its next run time
php artisan schedule:list
# Trigger a one-off run (does NOT respect overlapping guards)
php artisan schedule:test
# Simulate the scheduler tick locally
php artisan schedule:workIn production the OS cron must call:
cron
* * * * * cd /var/www/cpr-backend && php artisan schedule:run >> /dev/null 2>&1(In Docker/Sail, the queue container handles this; see compose.yaml.)
5. Pass arguments via array
For commands with options, pass them as an array — string concatenation is fragile:
php
// Good
Schedule::command('cpr:backup:database', ['--source=scheduled', '--keep-days=30'])
->dailyAt('03:00');
// Bad — quoting breaks on some shells
Schedule::command('cpr:backup:database --source=scheduled --keep-days=30')->dailyAt('03:00');6. Failure alerting
Sentry already captures uncaught exceptions in scheduled commands. For commands that should never be silent on failure:
php
Schedule::command('cpr:backup:database')
->dailyAt('03:00')
->onFailure(function () {
// Send Slack/Email notification
});Checklist
- [ ] Schedule defined in
routes/console.php - [ ]
withoutOverlapping()+onOneServer()for anything non-trivial - [ ] Cadence avoids
00:00and aligns with surrounding jobs - [ ] Options passed as an array, not concatenated
- [ ] Verified via
php artisan schedule:list
