skip to content

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%

answer

  1. sequential in definition order
  2. a slow task delays the rest
  3. command and exec only
  4. closures throw RuntimeException
  5. schedule:finish runs the hooks

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.

solid answer

~40 s

`schedule:run` executes due tasks **sequentially**, so a 20-minute `reports:generate` at midnight delays every task defined after it. `->runInBackground()` changes how a `Schedule::command()` or `Schedule::exec()` task is launched: the shell command is wrapped so it runs detached, with its output redirected, and `schedule:run` continues immediately. When the command ends, the wrapper calls the hidden `schedule:finish` command with the exit code; that process runs the task's after, success and failure hooks, releases any overlap lock, and fires `ScheduledBackgroundTaskFinished`. A `Schedule::call()` closure cannot be backgrounded — calling it throws a `RuntimeException` — so move such work into a command or a queued job. Queued `Schedule::job()` tasks need no background flag, because dispatching is already instant.

code

php · 13 lines
php
<?php

use Illuminate\Support\Facades\Schedule;

// Both start at 00:00; neither waits for the other
Schedule::command('reports:generate')->daily()->runInBackground();
Schedule::command('exports:customers')->daily()->runInBackground();

// Fast task stays in the foreground
Schedule::command('cache:prune-stale-tags')->daily();

// Would throw RuntimeException: closures cannot run in the background
// Schedule::call(fn () => ...)->daily()->runInBackground();

go deeper

for a junior

Remember that due tasks run one after another, and that runInBackground() lets a slow command or exec task stop blocking the rest.

for a middle

Explain the wrapper: detached subshell, output redirection, and schedule:finish running hooks and releasing locks with the exit code.

for a senior

Plan midnight load: background only independent slow tasks, keep dependent steps ordered, and monitor background failures through hooks or pings.

for a principal

Decide when bursts of scheduled processes should instead become queued work with bounded worker concurrency.

## The default: one after another When `schedule:run` starts, it collects the tasks due in that minute and runs them in the order they were defined in `routes/console.php`. Each foreground task must finish before the next begins: ```php Schedule::command('reports:generate')->daily(); // takes 20 minutes Schedule::command('cache:prune-stale-tags')->daily(); // starts at about 00:20 ``` The second task is not late because of cron; it is waiting for the first. With enough slow daily tasks, the last one in the file can start an hour after midnight. ## What runInBackground() changes ```php Schedule::command('reports:generate') ->daily() ->runInBackground(); ``` For a background task the scheduler builds a different shell command. In outline: 1. The task's command runs inside a subshell with its output redirected to the task's output file (or `/dev/null`). 2. After it ends, the subshell runs `php artisan schedule:finish "<mutex name>" "<exit code>"`. 3. The whole subshell is started with `&`, so the shell returns immediately and `schedule:run` moves on to the next due task. `schedule:finish` is a hidden command. It builds the schedule, finds the event whose mutex name matches, and calls the event's finish step with the exit code. That is where the after, `onSuccess()` and `onFailure()` hooks run, where a `withoutOverlapping()` lock is released, and where `ScheduledBackgroundTaskFinished` is dispatched. ## What it does not do | Task type | `runInBackground()` | | --- | --- | | `Schedule::command()` | Supported | | `Schedule::exec()` | Supported | | `Schedule::call()` closure | Throws `RuntimeException: Scheduled closures can not be run in the background.` | | `Schedule::job()` | Also a callback event, so it throws too — and a queued job needs no background flag | A closure runs inside the `schedule:run` PHP process; there is no separate process to detach. The options are to turn the closure into an Artisan command, or to make it dispatch a queued job so the worker does the slow part. ## Consequences to know - **Failure reporting moves.** For a foreground command, `schedule:run` sees the exit code and reports a non-zero one through the exception handler. For a background command it only knows the launch succeeded; the exit code arrives later in `schedule:finish`, so rely on `onFailure()` hooks or pings rather than the scheduler's own failure report. - **Hooks run in another process.** Anything the hook closure captured from the defining file is rebuilt in the `schedule:finish` process, not shared with `schedule:run`. - **Signal-based lock release does not apply.** The SIGTERM handler that frees an overlap lock is installed only for foreground tasks; a background task's lock is released by `schedule:finish`. - **Parallelism is unbounded.** Five background tasks due at midnight start five processes at once. That is the point, but it can also spike CPU and database connections. ## Choosing - Several independent slow commands at the same minute: add `runInBackground()` to the slow ones. - One slow task that others depend on (for example, reports that must exist before an email task sends them): keep it in the foreground, or chain the second step from the first. - Heavy closure work: move it to a command or a queued job, since closures cannot be backgrounded. ## A note on order Background tasks still **start** in definition order; they just do not wait. If one short task must always start first, define it first and keep it in the foreground, or give it its own earlier minute. ## Why juniors are asked this The question checks the mental model more than the method: the scheduler is a single short-lived process that works through a list, not a thread pool. Once that picture is in place, the need for `runInBackground()`, the restriction to processes, and the move of hooks into `schedule:finish` all follow from it.

  • Where do onSuccess() and onFailure() hooks run for a Laravel task scheduled with runInBackground()?
    In a separate `php artisan schedule:finish` process that the background wrapper starts after the command ends, passing its exit code. That process rebuilds the schedule, finds the event by its mutex name, runs the hooks, releases any overlap lock and dispatches `ScheduledBackgroundTaskFinished`.
  • How would you make a slow Laravel scheduled closure stop delaying the tasks after it?
    Closures cannot use `runInBackground()`, so change what the schedule runs: move the logic into an Artisan command and schedule that with `runInBackground()`, or have the schedule dispatch a queued job so a worker does the work and `schedule:run` returns immediately.

saying these in an interview costs you the question

  • Laravel runs every task due in the same minute in parallel by default.
  • runInBackground() works for scheduled closures as well as commands.
  • With runInBackground(), onFailure() hooks can no longer fire.
  • Background tasks are queued jobs that need a running queue worker.
  • Tasks after a slow foreground task run at their scheduled minute anyway.