skip to content

Chains & Batches

Bus::chain runs jobs in order and stops at the first failure, while Bus::batch runs them in parallel with then, catch and finally callbacks. Interviewers probe cancellation and partial failure.

on this pageshow

explore

questions

4

In Laravel, what is the difference between Bus::chain and Bus::batch, and when would you use each for queued jobs?

level: juniorimportance: must knowfreq 45%

answer

  1. in order versus in parallel
  2. a chain stops at the first failure
  3. a batch is tracked in job_batches
  4. both need an explicit ->dispatch()
  5. arrays nest chains inside batches

basics

~20 s

Bus::chain runs jobs one after another, each only if the previous one succeeded, and stops at the first failure. Bus::batch pushes all jobs at once to run in parallel, records progress in job_batches and fires then, catch and finally callbacks.

solid answer

~40 s

`Bus::chain([...])->dispatch()` queues only the first job; the rest travel inside it and each is pushed when its predecessor finishes successfully, so steps run **in order** and the chain stops at the first job that fails, with `catch()` as the hook. `Bus::batch([...])->dispatch()` stores a row in `job_batches`, pushes **every** job immediately so any number of workers can run them in parallel, and returns an `Illuminate\Bus\Batch` whose counters drive `then`, `catch`, `finally` and `progress` callbacks. Batched jobs need the `Batchable` trait. Use a chain for dependent steps (unzip, then publish), a batch for many independent units (resize 5,000 images), and combine them: an array inside a batch is a chain, and a `Bus::batch(...)` inside a chain is one step. Unlike a single job's `dispatch()`, neither pushes anything until you call `->dispatch()`.

code

php · 19 lines
php
<?php

use App\Jobs\PublishCollection;
use App\Jobs\ResizeImage;
use App\Jobs\UnpackUpload;
use Illuminate\Support\Facades\Bus;

// In order: unpack, then publish
Bus::chain([
    new UnpackUpload($upload),
    new PublishCollection($upload),
])->dispatch();

// In parallel: one job per image, tracked as a batch
$batch = Bus::batch(
    $upload->images->map(fn ($image) => new ResizeImage($image))
)->name("upload {$upload->id}")->dispatch();

return $batch->id;

go deeper

for a junior

Recall that a chain runs jobs in order and stops on failure, while a batch runs them in parallel with completion callbacks.

for a middle

Explain how a chain carries its remaining jobs in the first payload and how a batch tracks counters in job_batches.

for a senior

Design multi-step pipelines by nesting batches and chains, and account for failure behaviour at each level.

for a principal

Decide when chains and batches are the right workflow tool and when a pipeline needs a dedicated orchestration model instead.

## Two ways to group queued jobs A single queued job is one unit of work. Real features are often several: a stock-photo site that accepts a zip of 5,000 images must unpack the archive, create a thumbnail and a watermarked preview for each image, then publish the collection. Laravel's command bus offers two grouping tools through the `Illuminate\Support\Facades\Bus` facade. | | `Bus::chain` | `Bus::batch` | |---|---|---| | Execution | one job at a time, in order | all jobs at once, in parallel | | What is pushed at dispatch | the first job only | every job | | On failure | the remaining jobs never run | the batch is cancelled by default, others keep running unless they check | | Tracking | none beyond the jobs themselves | a row in `job_batches` with counters | | Callbacks | `catch()` | `before`, `progress`, `then`, `catch`, `finally` | | Job requirement | any queued job or closure | jobs using the `Batchable` trait | | Return of `dispatch()` | the first job's dispatch result | an `Illuminate\Bus\Batch` with an `id` | ## How a chain works `Bus::chain([new UnpackUpload($upload), new PublishCollection($upload)])->dispatch()` takes the first job, stores the serialized remaining jobs in its `chained` property, and dispatches it. When a worker finishes that job **successfully**, it pushes the next one with the rest of the chain attached. Consequences: - ordering is guaranteed, because the next step does not exist on the queue until the previous one succeeded; - if a step fails for good, the later steps are never pushed and the chain's `catch()` callbacks run; - `UnpackUpload::withChain([...])->dispatch($upload)` is the per-class spelling of the same thing; the arguments to `dispatch()` construct the first job. ## How a batch works `Bus::batch($jobs)->dispatch()` first creates a batch record — an id, `total_jobs`, `pending_jobs`, `failed_jobs`, the serialized callbacks — then stamps each job with the batch id and bulk-pushes them all. Each worker that finishes a job updates the counters; the last update decides which callbacks fire. The `Batch` object can be looked up later with `Bus::findBatch($id)` and is JSON-serializable, which makes a progress bar a one-line route. All jobs in one batch go to the same connection and queue. ## Combining them The two nest in both directions: 1. **Chains in a batch** — put an array inside the batch: `Bus::batch([[new MakeThumbnail($a), new MakePreview($a)], ...])`. Each array runs in order; the arrays run in parallel; every job in them counts toward the batch's totals. 2. **Batches in a chain** — put `Bus::batch([...])` as a step of `Bus::chain([...])`. The chain waits until that batch finishes before moving on. For the zip upload, a natural shape is `Bus::chain([new UnpackUpload($upload), Bus::batch($imageJobs), new PublishCollection($upload)])`. In practice the list of 5,000 image jobs is only known after unpacking, so the unpack job usually creates the batch itself. ## What travels in the payload The two tools store their bookkeeping in different places, which matters for payload size and debugging: - a **chain** serializes every remaining step into the job currently on the queue, so a ten-step chain carrying models ships ten serialized jobs in the first payload, shrinking by one at each step; - a **batch** stores its callbacks and options once in the `job_batches` row, and each job carries only the batch id; - models in either are stored as identifiers by `SerializesModels`, so they are re-fetched when each job runs; - a **name** set with `->name()` on a batch shows up in the tools that display batches, such as Horizon and Telescope. ## Traps that catch people - **Forgetting `->dispatch()`.** A single job's `dispatch()` pushes when its `PendingDispatch` is destroyed, but `PendingChain` and `PendingBatch` push only when you call `dispatch()` on them; without it, nothing is queued. - **Using `$this` in callbacks.** Chain and batch callbacks are serialized and run later on a worker; the docs warn against referring to `$this` inside them. - **Expecting ordered completion from a batch.** With several workers, jobs finish in any order. - **Missing `Batchable`.** A job without the trait cannot report to its batch; `make:job --batched` generates a stub with it. - **Deleting to stop a chain.** Calling `$this->delete()` inside a chain step does not stop the chain; only a failure does. ## Choosing Use a chain when step B needs the result of step A. Use a batch when many units are independent and you care about their combined outcome. When you need neither ordering nor a combined outcome, plain dispatches — or `Bus::bulk()`, added in Laravel 13.13 for pushing many jobs at once without batch tracking — are simpler.

  • How do you run the per-image work only after the zip is unpacked, and publish only after every image is done?
    Nest a batch inside a chain: `Bus::chain([new UnpackUpload($upload), Bus::batch($imageJobs), new PublishCollection($upload)])->dispatch()`. The chain treats the batch as one step and moves on only when it finishes. If the image list is only known after unpacking, let `UnpackUpload` dispatch the batch with a `then()` callback that queues the publish job.
  • Why does Bus::batch return a Batch object while Bus::chain returns what dispatching its first job returns?
    A batch has its own identity: a `job_batches` row with an id and counters, which you can look up with `Bus::findBatch()`. A chain is just the first job carrying the rest in its payload; there is nothing separate to track, so dispatching it returns what dispatching that first job returns.

saying these in an interview costs you the question

  • Jobs in a batch run one after another in the order they were added
  • A chain keeps running the later jobs after one of them fails
  • Bus::chain pushes every job in the chain to the queue at dispatch
  • Bus::batch queues its jobs even without calling dispatch()
  • Calling $this->delete() inside a chain step stops the rest of the chain
open as a page

In a Laravel Bus::batch, when exactly do then, catch and finally fire, and how does allowFailures() change what one failed job does?

level: seniorimportance: must knowfreq 40%

basics

~20 s

then fires only when every job has succeeded; catch fires on the first failed job; finally fires once every job has run. By default the first failure cancels the batch; allowFailures() keeps it going and can call a closure per failure.

open as a page

How do you make a Laravel job batchable, report a batch's progress to the UI, and cancel the batch from inside one of its jobs?

level: middleimportance: should knowfreq 35%

basics

~10 s

Add the Batchable trait (make:job --batched does it) so $this->batch() returns the batch. Return Bus::findBatch($id) from a route for progress JSON, and call $this->batch()->cancel() in a job; other jobs skip work by checking cancelled().

open as a page

In a Laravel job chain, what happens to the remaining jobs when one fails, and how do catch(), onQueue() and appendToChain() behave?

level: middleimportance: should knowfreq 38%

basics

~20 s

When a chained job fails for good, later jobs are never pushed and the chain's catch() callbacks run with the exception. onConnection() and onQueue() set defaults for every step; a running step can add work with prependToChain() or appendToChain().

open as a page