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?
answer
- child process versus in-process
- command builds php artisan string
- call runs inside schedule:run
- job with ShouldQueue only dispatches
- exec hands a string to the shell
basics
~20 sSchedule::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.
solid answer
~40 s`Schedule::command('subscriptions:renew')` (or a command class plus an arguments array) builds a `php artisan ...` string and runs it as a **child process**; its exit code decides success. `Schedule::exec('node script.js')` does the same for any shell command. `Schedule::call()` takes a closure or invokable object and runs it **inside the `schedule:run` process** through the container, so dependencies are injected; returning `false` or throwing marks it failed. `Schedule::job(new HealthPing)` wraps a dispatch: if the job implements `ShouldQueue` it is pushed to the queue (optional queue and connection arguments) and a **worker** does the work later; otherwise it is dispatched synchronously inside `schedule:run`. The choice decides isolation, where failures surface, and which scheduler options apply.
code
php · 18 lines<?php
use App\Console\Commands\RenewSubscriptions;
use App\Jobs\HealthPing;
use App\Services\CartService;
use Illuminate\Support\Facades\Schedule;
// Child process: php artisan subscriptions:renew --chunk=500
Schedule::command(RenewSubscriptions::class, ['--chunk' => 500])->dailyAt('02:00');
// Pushed to the 'heartbeats' queue; a worker runs it
Schedule::job(new HealthPing, 'heartbeats')->everyFiveMinutes();
// Runs inside schedule:run, dependencies injected
Schedule::call(fn (CartService $carts) => $carts->purgeStale())->hourly();
// Shell command in a child process
Schedule::exec('pg_dump app > /backups/app.sql')->dailyAt('03:00');go deeper
Recall the four methods and one line each: command runs Artisan, exec runs shell, call runs a closure, job dispatches a job.
Explain the process boundary: command and exec are child processes judged by exit code, call runs inside schedule:run, and job with ShouldQueue ends at the dispatch.
Choose by failure blast radius and retry needs: heavy or leaky work in a child process or a queued job, retries only through the queue, and workers provisioned for scheduled queues.
Set a team rule for scheduled work: when a closure is acceptable, when it must be a command or queued job, and how failures are monitored across the scheduler and the queue.
## Four ways to register a task Laravel's `Schedule` (the `Illuminate\Support\Facades\Schedule` facade over `Illuminate\Console\Scheduling\Schedule`) offers four entry points. They look alike in `routes/console.php`, but they put the work in different processes, and that is what interviewers probe. | Method | What it registers | Where the work runs | What counts as failure | | --- | --- | --- | --- | | `Schedule::command()` | An Artisan command, by name string or class | A child `php artisan ...` process started by `schedule:run` | Non-zero exit code | | `Schedule::exec()` | Any shell command string | A child shell process | Non-zero exit code | | `Schedule::call()` | A closure or invokable object | Inside the `schedule:run` PHP process | Returns `false` or throws | | `Schedule::job()` | A job instance or class name | A queue worker if `ShouldQueue`, otherwise inline in `schedule:run` | Dispatch throws (queued); job throws (inline) | ## Schedule::command and Schedule::exec — child processes `Schedule::command('subscriptions:renew --chunk=500')` turns the string into a full `php artisan subscriptions:renew --chunk=500` command line. You may also pass the class and an argument array: `Schedule::command(RenewSubscriptions::class, ['--chunk' => 500])`; the scheduler resolves the class to read its name and description. `Schedule::exec()` skips the Artisan part and hands the string to the shell. Both are run through Symfony Process as a separate operating-system process. That gives you: - **Isolation** — a memory leak or fatal error kills only the child; `schedule:run` records the exit code and moves on. - **A fresh application** — for Artisan commands, the child boots its own copy of Laravel. - **Exit-code semantics** — the task fails when the process exits non-zero, which is what success and failure hooks key on. Closure commands declared with `Artisan::command()` can be scheduled by chaining a frequency straight onto the definition (`->purpose('...')->daily()`), which calls `Schedule::command()` under the hood. ## Schedule::call — in-process closures `Schedule::call(function (SubscriptionService $renewals) { ... })->daily()` runs the closure **inside the `schedule:run` process**. The container invokes it, so type-hinted dependencies are injected, and an invokable class works too: `Schedule::call(new PurgeStaleCarts)`. Consequences: 1. It is cheap — no second PHP boot. 2. It shares the scheduler's memory. A thrown exception is caught, reported and the remaining due tasks still run, but an uncatchable fatal error such as memory exhaustion ends `schedule:run` itself, and the tasks after it in that minute do not run. 3. It has no process exit code; the scheduler treats a return value of `false` or an exception as failure. 4. Some options that only make sense for a process are unavailable for closures, and closures need an explicit `->name()` before certain locking options — `schedule:list` otherwise shows them only by file and line. ## Schedule::job — a dispatch, not an execution `Schedule::job(new HealthPing, 'heartbeats', 'redis')->everyFiveMinutes()` registers a small callback that **dispatches** the job. The second and third arguments pick the queue and connection; when omitted, the job's own `$queue` and `$connection` properties apply. - If the job implements `ShouldQueue`, the scheduler's part ends when the job is pushed; a queue worker executes it later. Its retries and failures belong to the queue system, not to the scheduler. - If it does not implement `ShouldQueue`, it is dispatched synchronously and runs inside `schedule:run`, with the same risks as `Schedule::call()`. The event is automatically named after the job (its class name, or its `displayName()` when it defines one), which is what `schedule:list` displays. Unless a worker is running for that connection and queue, a scheduled queued job piles up unprocessed while the scheduler reports success every time. ## Choosing between them - Heavy, long or memory-hungry work on a schedule: `Schedule::command()` or `Schedule::job()` with a queued job. - A two-line database cleanup: `Schedule::call()` is fine. - A non-PHP tool (a backup binary, a Node script): `Schedule::exec()`. - Work that should be retried with backoff: a queued job, because the scheduler itself never retries. ## What interviewers are really checking The question is less about syntax than about failure modes. A strong answer names the process each task lives in and draws the consequences: - **Where do errors surface?** Child-process failures show up as exit codes and captured output; closure failures as reported exceptions; queued-job failures in the queue's failed-job handling, not in the scheduler at all. - **What shares memory?** Only `Schedule::call()` and a non-queued `Schedule::job()` share the scheduler's process, so only they can take the rest of the minute down with them. - **What does "success" mean?** For a queued job, success means "pushed", which is why a schedule can look green while no worker is consuming the queue.
- A Laravel scheduled closure registered with Schedule::call exhausts PHP's memory limit. What happens to the other tasks due that minute?Memory exhaustion is a fatal error, not a catchable exception, so it ends the `schedule:run` process itself; tasks defined after the closure do not run in that minute. A `Schedule::command()` task hitting the same limit would only kill its child process, and `schedule:run` would record the non-zero exit code and continue.
- Does Schedule::job in Laravel retry a queued job that fails on the worker?The scheduler does not; it only dispatched the job and considered its part done. Retries, backoff and failed-job handling come from the job's own queue settings and the worker running it. The scheduler would only record a failure if the dispatch itself threw, for example because the queue connection was unreachable.
saying these in an interview costs you the question
- Schedule::job always runs the job inside the schedule:run process.
- Schedule::call starts a separate PHP process for the closure.
- An exception thrown by one scheduled closure stops every later task in that minute.
- Schedule::command calls the command's handle() method directly in the scheduler.
- A scheduled queued job runs even when no queue worker is running.