skip to content

Task Scheduler

Laravel's scheduler defines recurring commands, jobs and closures in code and runs them from a single cron entry. Interviewers probe overlap, running once across servers and failure monitoring.

on this pageshow

explore

questions

12

In Laravel 13, where do you define scheduled tasks, and how does a single cron entry running schedule:run execute all of them?

level: juniorimportance: must knowfreq 78%

answer

  1. a routes file, not the crontab
  2. Schedule facade or withSchedule
  3. cron fires every minute
  4. schedule:run runs only what is due now
  5. schedule:work locally, schedule:list to inspect

basics

~20 s

Tasks are registered with the Schedule facade in routes/console.php, or through withSchedule in bootstrap/app.php. One server cron entry runs php artisan schedule:run every minute, and that command runs whichever tasks are due in that minute.

solid answer

~50 s

In Laravel 13 the schedule lives in code: `routes/console.php` calls `Schedule::command()`, `Schedule::job()`, `Schedule::call()` or `Schedule::exec()` and chains a frequency such as `->daily()`, or you register it with `->withSchedule(fn (Schedule $schedule) => ...)` in `bootstrap/app.php`. The server needs exactly one crontab line, `* * * * * cd /path-to-app && php artisan schedule:run >> /dev/null 2>&1`. Each minute `schedule:run` boots the app, builds the schedule, keeps the tasks whose cron expression matches the current minute (and whose environment and filters pass), runs them and exits. Nothing catches up: a minute in which cron did not fire is simply lost. Locally you run `php artisan schedule:work` instead of a cron entry, and `php artisan schedule:list` shows every task with its expression and next run time. The old `app/Console/Kernel.php` `schedule()` method is not part of the current skeleton.

code

php · 11 lines
php
<?php

// routes/console.php
use App\Jobs\HealthPing;
use Illuminate\Support\Facades\Schedule;

Schedule::command('subscriptions:renew')
    ->dailyAt('02:00')
    ->environments(['production']);

Schedule::job(new HealthPing, 'heartbeats')->everyFiveMinutes();

go deeper

for a junior

Recall the file (routes/console.php), the Schedule facade, and the exact every-minute cron line that runs php artisan schedule:run.

for a middle

Explain what schedule:run does each minute: build the schedule, keep due tasks by expression, environment and filters, run them in order, then exit — and why there is no catch-up.

for a senior

Talk about operating it: one identical cron line per scheduler host, cheap definitions because every minute boots the app, schedule:list in deploy checks, and designing tasks to tolerate a skipped minute.

for a principal

Weigh the in-code schedule against platform schedulers: reviewable definitions and app context versus a per-minute boot cost and reliance on a healthy cron on every host.

## Where the schedule lives Laravel's **task scheduler** moves the list of recurring jobs out of the server's crontab and into the application's source code, where it is versioned, reviewed and deployed with everything else. In a Laravel 13 application there are two supported places to declare it: - **`routes/console.php`** — the default. The skeleton's `bootstrap/app.php` loads this file through `withRouting(commands: __DIR__.'/../routes/console.php')`, and you call the `Illuminate\Support\Facades\Schedule` facade in it. - **`bootstrap/app.php`** — the `withSchedule()` method of the application builder accepts a closure that receives an `Illuminate\Console\Scheduling\Schedule` instance, for teams that want `routes/console.php` to hold only closure commands. Each declaration starts with one of four methods — `Schedule::command()` for an Artisan command, `Schedule::job()` for a job class, `Schedule::call()` for a closure or invokable object, `Schedule::exec()` for a shell command — and then chains a **frequency** (`->daily()`, `->everyFiveMinutes()`, `->cron('0 2 * * *')`) plus optional constraints. For this leaf's running example, nightly subscription renewals and a five-minute health ping: ```php use App\Jobs\HealthPing; use Illuminate\Support\Facades\Schedule; Schedule::command('subscriptions:renew')->dailyAt('02:00'); Schedule::job(new HealthPing)->everyFiveMinutes(); ``` Older applications declared the same thing inside a `schedule(Schedule $schedule)` method on `app/Console/Kernel.php`. The current skeleton ships no such file; the framework's own console kernel still exists, but you no longer edit it. ## The one cron entry The operating system still has to wake something up, and that something is a single crontab line: ```bash * * * * * cd /path-to-app && php artisan schedule:run >> /dev/null 2>&1 ``` It fires **every minute**, whatever your tasks' frequencies are. Hourly or daily tasks do not need an hourly or daily cron line — the scheduler decides, minute by minute, whether each task is due. Running the entry less often (say `0 * * * *`) silently drops every task whose minute does not line up. ## What schedule:run does each minute `schedule:run` is a short-lived command, not a daemon. On each invocation it: 1. Boots the application, which loads `routes/console.php` (and runs any `withSchedule` callback), registering every task on the `Schedule`. 2. Keeps the **due** events: the cron expression matches the current minute in the task's timezone, the task's `environments()` list (if any) contains the current `APP_ENV`, and the app is not in maintenance mode unless the task opted in. 3. For each due event, evaluates its `when()`/`skip()` filters, then runs it — by default one after another, in the order they were defined. 4. Reports failures through the exception handler and fires `ScheduledTaskStarting`, `ScheduledTaskFinished`, `ScheduledTaskFailed` or `ScheduledTaskSkipped` events, then exits. If nothing was due it prints `No scheduled commands are ready to run.` (suppressed by `--whisper`). Two consequences follow. First, there is **no catch-up**: if cron was stopped between 00:00 and 00:10, a task due at 00:00 does not run at 00:10. Second, an exception in one task does not stop the rest of that minute's tasks, because each run is wrapped and reported individually; only an uncatchable fatal error inside an in-process closure can end the command early. When sub-minute tasks are due, the command deliberately stays alive until the end of its minute instead of exiting after one pass. ## Local development and inspection | Command | What it does | | --- | --- | | `php artisan schedule:run` | Evaluates and runs what is due right now, then exits | | `php artisan schedule:work` | Foreground loop that starts `schedule:run` at the top of every minute until you stop it | | `php artisan schedule:list` | Lists each task, its cron expression and its next due time | | `php artisan schedule:test` | Lets you pick one scheduled command and run it immediately | `schedule:work` replaces the crontab on a laptop: it spawns `schedule:run` as a subprocess each minute and, on Ctrl+C, stops scheduling new runs while letting in-flight ones finish. `schedule:list` accepts `--timezone`, `--next` (sort by next due date), `--json` and, in Laravel 13, `--environment` to show only the tasks registered for one environment. ## Why this design - **Source control:** adding the renewals task is a pull request, not an SSH session. - **One moving part on the server:** provisioning scripts install one identical line everywhere. - **Application context:** tasks run with the app's config, container and logging. - **Cost:** every minute boots the framework once, so keep the definitions file cheap — no queries or HTTP calls at the top level of `routes/console.php`.

  • What happens in Laravel to a daily task whose minute passed while cron was not running?
    Nothing — it is skipped for that day. `schedule:run` only evaluates the minute it is started in; it keeps no record of missed ticks and never replays them. If a renewal run must not be lost, make the task itself catch up, for example by renewing every subscription whose renewal date is at or before now rather than only those dated today.
  • How do you run Laravel's scheduler on a development machine without adding a cron entry?
    Run `php artisan schedule:work` in a terminal. It stays in the foreground, starts a `schedule:run` subprocess at second zero of every minute, streams its output, and on Ctrl+C stops starting new runs while letting a running one finish. `--run-output-file` sends the runs' output to a file instead.
  • How can you confirm which tasks Laravel has registered and when each will next run?
    `php artisan schedule:list` prints each task's cron expression, command or closure location, and next due time. `--next` sorts by next due date, `--timezone` converts the display, `--json` gives machine-readable output, and `--environment` limits the list to tasks that would run in a given environment. Closures show their file and line unless you name them.

A station clock that chimes once a minute: at each chime the stationmaster reads the timetable and sends off only the trains due at that minute. If the clock stops for ten minutes, the trains due in that gap are not sent later; they are simply missed.

saying these in an interview costs you the question

  • Each scheduled task needs its own line in the server's crontab.
  • The cron entry should run hourly because the tasks are hourly or daily.
  • schedule:run is a daemon that a process supervisor must keep alive.
  • New Laravel 13 apps define tasks in app/Console/Kernel.php's schedule() method.
  • Laravel replays tasks that were missed while cron was stopped.
open as a page

In Laravel's scheduler, how do Schedule::command, Schedule::job, Schedule::call and Schedule::exec differ, and in which process does each task's work run?

level: middleimportance: must knowfreq 52%

basics

~20 s

Schedule::command and Schedule::exec start a child process from schedule:run; Schedule::call runs a closure inside the schedule:run process; Schedule::job dispatches the job, so a ShouldQueue job runs later on a queue worker, while a non-queued one runs inline.

open as a page

In Laravel's scheduler, what does withoutOverlapping() do, where is its lock stored, and how long does that lock last by default?

level: middleimportance: must knowfreq 55%

basics

~20 s

withoutOverlapping() makes the scheduler take a cache lock before running a task and skip the task while that lock exists, so a slow run is not joined by a second copy. The lock expires after 1440 minutes unless you pass another value.

open as a page

When a Laravel app scaled to three servers runs schedule:run on each, why does a report task run three times, and how does onOneServer() prevent it?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Each server's cron runs schedule:run, and each sees the same task due, so it runs once per server. onOneServer() makes each server try a lock in a shared cache keyed by task and minute; only the winner runs it.

open as a page

In Laravel's scheduler, do tasks due in the same minute run in parallel, and what changes when you add runInBackground() to one?

level: juniorimportance: should knowfreq 40%

basics

~10 s

No: schedule:run runs due tasks one after another, in the order they are defined. runInBackground() starts a command or exec task as a detached process and moves on at once; closures cannot use it.

open as a page

In a Laravel schedule, how do frequency helpers such as daily() and everyFiveMinutes() build the cron expression, and why does chaining order matter?

level: middleimportance: should knowfreq 38%

basics

~20 s

Every task starts with the expression * * * * *, and each helper overwrites only the cron fields it owns. Later calls win on shared fields, so ->daily()->everyFiveMinutes() becomes */5 0 * * * — every five minutes during the midnight hour only.

open as a page

In Laravel's scheduler, which timezone decides when dailyAt('02:00') fires, and how do the timezone() method and app.schedule_timezone change it?

level: middleimportance: should knowfreq 42%

basics

~20 s

A task's time is matched against the clock in its timezone: the task's own timezone() if set, otherwise config app.schedule_timezone, otherwise app.timezone, which the skeleton sets to UTC. The server's operating-system timezone plays no part.

open as a page

In Laravel's scheduler, how do environments(), when() and skip() decide whether a due task actually runs, and when is each one evaluated?

level: middleimportance: should knowfreq 36%

basics

~20 s

environments() is checked with the cron expression when schedule:run picks due tasks and needs an exact APP_ENV match. For each due task, every when() callback must return true and every skip() callback false, evaluated at that moment through the container.

open as a page

How do you capture a Laravel scheduled command's output and get alerted when it fails, using sendOutputTo(), onFailure() and thenPing()?

level: middleimportance: should knowfreq 40%

basics

~20 s

Output of scheduled commands is discarded unless you chain sendOutputTo() or appendOutputTo(). onSuccess()/onFailure() run on exit code zero or non-zero and can receive the output as a Stringable; thenPing() and pingOnFailure() hit URLs for external monitors.

open as a page

Since cron fires at most once a minute, how does Laravel's scheduler run a task defined with everyTenSeconds(), and what are the pitfalls?

level: seniorimportance: should knowfreq 30%

basics

~20 s

When any due task uses a second-based helper, schedule:run does not exit after its first pass: it loops until the end of its minute, re-running each repeating task once its interval has passed. Everything runs sequentially, so slow sub-minute work should be queued.

open as a page

A Laravel task using withoutOverlapping() stopped running after its scheduler container was force-killed mid-run; why, and how do you recover and prevent it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The schedule:run process died before it could delete the task's overlap lock, so every later tick finds the lock and skips the task until its TTL, 24 hours by default, expires. Clear it with schedule:clear-cache, then set a shorter expiry and monitor.

open as a page

During a Laravel deploy that uses php artisan down, what happens to scheduled tasks, and what do evenInMaintenanceMode() and schedule:interrupt change?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

While the app is in maintenance mode, scheduled tasks are treated as not due and silently do not run, unless marked evenInMaintenanceMode(). schedule:interrupt tells an already running sub-minute schedule:run loop to stop early, so it does not keep executing the previous release.

open as a page