Introduction
Defining a task with ->daily() is the easy part. Making it production-ready — not overlapping itself, running on only one server in a fleet, alerting on failure, inheriting timezone and lock config from a group — is where the advanced features earn their keep.
Key Concepts
withoutOverlapping($minutes): Takes a cache lock before running so the previous run finishes before the next one starts.onOneServer(): Atomic cache-based election that ensures only one server in a fleet runs the task.- Task hooks:
before(),after(),onSuccess(),onFailure()let you wire arbitrary side effects around a task. - Schedule group: A Laravel 13 feature that shares config (timezone,
onOneServer,at, etc.) across multiple tasks in one closure.
Real World Context
A daily backup that runs on a 4-server fleet must run exactly once. A weekly report that takes 20 minutes must not overlap with the next run. A critical task that silently fails at 3am is worse than one that fails loudly and pages someone. Every feature in this lesson exists to handle a specific production incident.
Deep Dive
Preventing Task Overlaps
phpSchedule::command('emails:send') ->withoutOverlapping() // Skip if previous still running ->daily(); // With lock expiration Schedule::command('reports:generate') ->withoutOverlapping(10) // Lock expires after 10 minutes ->hourly();
Running on One Server
For multi-server deployments:
phpSchedule::command('report:generate') ->onOneServer() // Only one server runs this ->daily();
Requires a cache driver that supports locks (Redis, Memcached, database).
Background Tasks
php// Run in background (don't block other tasks) Schedule::command('analytics:import') ->runInBackground() ->daily();
Conditional Scheduling
php// Only run when condition is true Schedule::command('emails:send') ->daily() ->when(function () { return config('mail.enabled'); }); // Skip when condition is true Schedule::command('backup:run') ->daily() ->skip(function () { return app()->isDownForMaintenance(); }); // Only in certain environments Schedule::command('telescope:prune') ->environments(['staging', 'production']) ->daily();
Day Constraints
php// Only on weekdays Schedule::command('report:generate') ->weekdays() ->at('08:00'); // Only on weekends Schedule::command('cleanup:run') ->weekends() ->at('02:00'); // Specific days Schedule::command('task') ->days([Schedule::MONDAY, Schedule::WEDNESDAY, Schedule::FRIDAY]) ->at('09:00');
Time Constraints
php// Only between certain hours Schedule::command('process:queue') ->everyMinute() ->between('8:00', '17:00'); // Business hours // Except between certain hours Schedule::command('heavy:task') ->hourly() ->unlessBetween('9:00', '17:00'); // Not during business hours
Task Output
php// Write output to file Schedule::command('emails:send') ->daily() ->sendOutputTo('/var/log/email-output.log'); // Append to file Schedule::command('emails:send') ->daily() ->appendOutputTo('/var/log/email-output.log'); // Email output Schedule::command('report:generate') ->daily() ->emailOutputTo('admin@example.com'); // Email only on failure Schedule::command('backup:run') ->daily() ->emailOutputOnFailure('admin@example.com');
Task Hooks
phpSchedule::command('emails:send') ->daily() ->before(function () { Log::info('Starting email send...'); }) ->after(function () { Log::info('Email send complete.'); }) ->onSuccess(function () { Notification::send($admin, new TaskSucceeded('emails:send')); }) ->onFailure(function () { Notification::send($admin, new TaskFailed('emails:send')); });
Ping URLs (Heartbeats)
php// Ping URL before/after task Schedule::command('backup:run') ->daily() ->pingBefore('https://healthchecks.io/ping/start') ->thenPing('https://healthchecks.io/ping/end') ->pingOnSuccess('https://healthchecks.io/ping/success') ->pingOnFailure('https://healthchecks.io/ping/failure');
Viewing Scheduled Tasks
bash# List all scheduled tasks with their next run time php artisan schedule:list
The command prints every task defined in routes/console.php along with its cron expression and the next scheduled run time, which is invaluable for sanity-checking a complex schedule before deploying it.
Complete Example
php// routes/console.php use Illuminate\Support\Facades\Schedule; // Daily tasks Schedule::command('backup:clean')->daily()->at('01:00'); Schedule::command('backup:run')->daily()->at('02:00'); // Email reports Schedule::command('report:daily') ->dailyAt('08:00') ->weekdays() ->emailOutputOnFailure('admin@example.com'); // Cleanup Schedule::command('telescope:prune --hours=48') ->daily() ->environments(['production']); Schedule::command('queue:prune-failed --hours=168') ->weekly(); // Processing Schedule::command('process:pending-orders') ->everyFiveMinutes() ->withoutOverlapping() ->onOneServer(); // Maintenance (off-hours only) Schedule::command('db:optimize') ->weekly() ->sundays() ->at('03:00') ->unlessBetween('08:00', '22:00');
Schedule Groups (Laravel 13)
Laravel 13 added schedule groups — a way to share configuration across several tasks without repeating it on each line. Call a frequency/config chain, then pass a closure containing the actual commands:
phpSchedule::daily() ->onOneServer() ->timezone('America/New_York') ->at('09:00') ->group(function () { Schedule::command('emails:send --force'); Schedule::command('reports:generate'); Schedule::command('analytics:sync'); });
All three commands inherit daily(), onOneServer(), timezone('America/New_York'), and at('09:00'). Before 13, you had to duplicate those four modifiers on every line — which was error-prone whenever one task drifted out of sync with the rest.
Common Pitfalls
- Using
filecache withonOneServer()— the "one server" guarantee relies on an atomic cache lock. A per-serverfilecache means every server thinks it holds the lock. Use Redis, Memcached, or a shared database cache. withoutOverlapping()without an expiration — if a task crashes mid-run, the lock stays held forever. Always pass a number of minutes:withoutOverlapping(30).
Best Practices
- Pair
onOneServer()withwithoutOverlapping($minutes)— the combination gives you "at most one, and never stuck". - Wire
onFailure()to your alerting channel — silent scheduled task failures are one of the most common "why didn't we notice?" postmortems.
Summary
withoutOverlapping($minutes)andonOneServer()are the two foundational production guards.before/after/onSuccess/onFailurehooks wire cross-cutting logic around tasks.- Laravel 13's schedule groups collapse shared configuration into one closure.
schedule:listprints every task with its next run time for sanity-checking.