In Laravel 13, where do you define scheduled tasks, and how does a single cron entry running schedule:run execute all of them?
answer
- a routes file, not the crontab
- Schedule facade or withSchedule
- cron fires every minute
- schedule:run runs only what is due now
- schedule:work locally, schedule:list to inspect
basics
~20 sTasks 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 sIn 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
// 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
Recall the file (routes/console.php), the Schedule facade, and the exact every-minute cron line that runs php artisan schedule:run.
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.
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.
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.