skip to content

Queues & Jobs

Laravel pushes work to queued jobs: dispatching and uniqueness, worker processes, retries and failed jobs, chains and batches, and Horizon on Redis. Interviewers ask what makes a job safe to rerun.

on this pageshow

explore

questions

24

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 Laravel, how do you create a queued job with make:job and dispatch it so it runs on a worker instead of inside the request?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Run php artisan make:job to get a class in app/Jobs that implements ShouldQueue and uses the Queueable trait, then call YourJob::dispatch($args). The job is serialized onto the default connection (database in a new app) and a queue:work process later calls handle().

open as a page

In Laravel, which Artisan commands list, retry and delete failed queued jobs, and what does queue:retry actually do with a failed job?

level: juniorimportance: must knowfreq 56%

basics

~10 s

php artisan queue:failed lists failed_jobs, queue:retry <id> or all pushes the stored payload back onto its original queue, queue:forget <id> deletes one record, queue:flush deletes all, and queue:prune-failed removes old ones.

open as a page

In Laravel, what is the difference between php artisan queue:work and php artisan queue:listen, and which belongs in production?

level: juniorimportance: must knowfreq 62%

basics

~20 s

queue:work boots the app once and runs jobs in one long-lived process: fast, but blind to new code until restarted. queue:listen spawns a fresh queue:work --once per job: current code, much slower. Production runs queue:work.

open as a page

In Laravel, why can a job dispatched inside DB::transaction during sign-up fail on the worker, and how does afterCommit fix it?

level: middleimportance: must knowfreq 58%

basics

~20 s

The job can reach a worker before the transaction commits, so the worker cannot see the new user and SerializesModels throws ModelNotFoundException. afterCommit holds the push until every open transaction commits, and drops the job on rollback.

open as a page

In a Laravel 13 queued job, how do tries, backoff and retryUntil control retries, and what happens after the last attempt?

level: middleimportance: must knowfreq 64%

basics

~20 s

Tries caps total attempts (one by default), backoff sets the seconds before an exception-triggered retry, and retryUntil swaps the count for a deadline. After the last attempt the job is deleted, failed() runs and failed_jobs records it.

open as a page

In Laravel Horizon, how do the environments, defaults and supervisor entries in config/horizon.php decide which worker processes start on a server?

level: middleimportance: must knowfreq 50%

basics

~10 s

Horizon picks the environments entry matching the app environment, merges each supervisor in it over the defaults block, and starts one supervisor per entry, each running its own Redis worker pool for its queues.

open as a page

After deploying a Laravel video-transcoding app, queue workers still run the old code; why, and how do queue:restart and Supervisor fix it?

level: middleimportance: must knowfreq 56%

basics

~20 s

queue:work loaded the old code at start and never reloads it. php artisan queue:restart writes a timestamp to the cache; each worker sees it, finishes its current job and exits, and Supervisor starts a fresh one.

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

With Laravel Horizon, a burst of notification jobs leaves billing jobs waiting although one auto-balanced supervisor serves both queues; how do balance strategies explain this, and how would you fix it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Auto balancing splits a supervisor's maxProcesses by queue load, not list order, so a notifications flood takes most workers while billing keeps minProcesses. Give billing its own supervisor with a guaranteed floor, or use balance false for strict order.

open as a page

In Laravel, how do a queue connection's retry_after and queue:work --timeout interact, and how can a misconfiguration make one transcode job run twice?

level: seniorimportance: must knowfreq 50%

basics

~20 s

retry_after is how long a reserved job may run before the backend re-offers it; --timeout is how long the worker allows before killing itself. If a job outlives retry_after and --timeout is longer, a second worker runs it too.

open as a page

Why does the Laravel Horizon dashboard at /horizon return 403 in production after install, and how does the viewHorizon gate grant access?

level: juniorimportance: should knowfreq 35%

basics

~20 s

Outside the local environment Horizon's dashboard is allowed only when the viewHorizon gate passes, and the gate published into App\Providers\HorizonServiceProvider starts with an empty email list, so everyone gets 403 until you fill it in.

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

In a Laravel queued job, what does SerializesModels store for an Eloquent model passed to the constructor, and what happens on the worker?

level: middleimportance: should knowfreq 52%

basics

~20 s

SerializesModels stores only a model identifier — class, primary key, loaded relation names and connection. On the worker the model is re-queried from the database, so the job sees current data, loses unsaved changes, and fails if the row was deleted.

open as a page

In Laravel, how do dispatch(), dispatchSync(), dispatchAfterResponse() and dispatchIf() differ in where and when a job's handle() runs?

level: middleimportance: should knowfreq 45%

basics

~20 s

dispatch() queues a ShouldQueue job for a worker; dispatchSync() runs it right now in the current process; dispatchAfterResponse() runs it in the same process after the response is sent; dispatchIf() and dispatchUnless() dispatch only when a condition holds.

open as a page

Inside a Laravel queued job's handle() method, what is the difference between calling $this->release(), calling $this->fail() and throwing an exception?

level: middleimportance: should knowfreq 44%

basics

~20 s

$this->release($delay) requeues the job for a later attempt, $this->fail() marks it failed immediately whatever tries remain, and throwing lets the worker retry with backoff until tries run out. Neither call stops handle(), so return afterwards.

open as a page

How does Laravel Horizon tag queued jobs automatically, and what must you schedule so its metrics dashboard shows throughput and runtime graphs?

level: middleimportance: should knowfreq 30%

basics

~10 s

Horizon tags a job at dispatch with Class:key for every Eloquent model in its properties, unless the job defines tags(), and its metric graphs fill only when horizon:snapshot is scheduled, typically every five minutes.

open as a page

In Laravel 13's config/queue.php, which queue drivers ship, and when would you move off the default database driver?

level: middleimportance: should knowfreq 42%

basics

~20 s

The skeleton defines sync, database, beanstalkd, sqs, redis, deferred, background and failover connections, defaulting to database. Move to Redis or SQS when polling the jobs table loads the main database, throughput grows, or you want Horizon.

open as a page

How does Laravel's php artisan queue:work --queue=high,default prioritise jobs, and what can go wrong with that setup?

level: middleimportance: should knowfreq 38%

basics

~20 s

On every loop the worker checks the listed queues in order and takes the first job found, so default runs only when high is empty. A busy high queue starves the rest, and unlisted queues are never polled.

open as a page

In Laravel, how do ShouldBeUnique and uniqueId prevent duplicate queued jobs, and why might a unique job silently stop being dispatched?

level: seniorimportance: should knowfreq 44%

basics

~20 s

ShouldBeUnique makes dispatch take a cache lock keyed by job class and uniqueId(); if it is held, the dispatch is silently skipped. The lock is freed when the job completes or finally fails, so a lost job can block later dispatches.

open as a page

In Laravel, when would you attach the RateLimited, WithoutOverlapping or ThrottlesExceptions job middleware, and how does each one affect a job's attempts?

level: seniorimportance: should knowfreq 30%

basics

~20 s

RateLimited enforces a known quota through a named limiter, WithoutOverlapping lets one job per key run at a time using a cache lock, and ThrottlesExceptions backs off after repeated exceptions. All three release jobs, and each release consumes an attempt.

open as a page

A Laravel job that geocodes imported property listings uses the RateLimited job middleware, and during a large import many jobs land in failed_jobs with MaxAttemptsExceededException although handle() never threw — why, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Every release by RateLimited consumes an attempt, and with the default of one try the next pickup exceeds the limit and fails. Give the job a retryUntil() deadline plus #[MaxExceptions] so real errors still fail fast.

open as a page

When deploying a Laravel app that runs Horizon, why must you run php artisan horizon:terminate, and what does it do to workers mid-job?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Horizon workers are long-running processes that booted the old code, so horizon:terminate sends SIGTERM to the local master; workers finish their current job and exit, and the process monitor restarts Horizon on the new release.

open as a page