In Laravel, what is the difference between Bus::chain and Bus::batch, and when would you use each for queued jobs?
answer
- in order versus in parallel
- a chain stops at the first failure
- a batch is tracked in job_batches
- both need an explicit ->dispatch()
- arrays nest chains inside batches
basics
~20 sBus::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
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
Recall that a chain runs jobs in order and stops on failure, while a batch runs them in parallel with completion callbacks.
Explain how a chain carries its remaining jobs in the first payload and how a batch tracks counters in job_batches.
Design multi-step pipelines by nesting batches and chains, and account for failure behaviour at each level.
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