skip to content

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%

answer

  1. app/Jobs, generated by Artisan
  2. one interface marks it queued
  3. Queueable bundles four traits
  4. dispatch() returns a PendingDispatch
  5. QUEUE_CONNECTION=database in the skeleton

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().

solid answer

~40 s

`php artisan make:job GenerateInvoicePdf` writes `app/Jobs/GenerateInvoicePdf.php`, which implements `Illuminate\Contracts\Queue\ShouldQueue` and uses `Illuminate\Foundation\Queue\Queueable` (a bundle of `Dispatchable`, `InteractsWithQueue`, the bus `Queueable` and `SerializesModels`). Constructor arguments are the job's data; `handle()` does the work and can type-hint services for injection. `GenerateInvoicePdf::dispatch($invoice)` passes the arguments to the constructor and returns a `PendingDispatch`, so you can chain `->onQueue('billing')`, `->onConnection('redis')` or `->delay(...)`; the push itself happens when that object is destroyed. A new Laravel 13 app queues to the `database` connection (a row in the `jobs` table), so nothing runs until a worker is consuming that queue. Leave out `ShouldQueue` and `dispatch()` simply runs `handle()` in the current process.

code

bash · 1 line
bash
php artisan make:job GenerateInvoicePdf

go deeper

for a junior

Recall the Artisan command, the ShouldQueue interface, the Queueable trait and the static dispatch call, and know that a worker has to be running.

for a middle

Explain that dispatch() returns a PendingDispatch that pushes in its destructor, and how connection, queue and delay are chosen per call or per class.

for a senior

Show you know what crosses the process boundary: constructor data is serialized, handle() runs later against current data, and routing decides which workers see the job.

for a principal

Frame which work belongs on a queue at all, and how queue names map to worker pools so slow jobs cannot starve urgent ones.

## What make:job generates A **job** in Laravel is a plain PHP class that describes one unit of background work. `php artisan make:job GenerateInvoicePdf` creates it in `app/Jobs` (the directory is created on first use). In Laravel 13 the generated class: - implements `Illuminate\Contracts\Queue\ShouldQueue` — the **marker interface** that tells the bus dispatcher to push the job onto a queue instead of running it now; - uses `Illuminate\Foundation\Queue\Queueable`, one trait that pulls in `Dispatchable` (the static `dispatch()` family), `InteractsWithQueue` (`release()`, `delete()`, `attempts()` on the running job), the bus `Queueable` (connection, queue, delay, chain settings) and `SerializesModels`; - has an empty constructor and a `handle(): void` method. The data the job needs goes into the **constructor**, usually as promoted public properties. Services go into `handle()` as type-hinted parameters, and the service container injects them when the worker runs the job. `make:job --sync` generates the opposite: a class with only `Dispatchable` and no `ShouldQueue`, meant to run in-process. ## What dispatch() actually does 1. `GenerateInvoicePdf::dispatch($invoice)` calls `new static(...$arguments)`, so the arguments go straight to the constructor. 2. It wraps the job in a `Illuminate\Foundation\Bus\PendingDispatch` and returns it. Nothing has been queued yet. 3. You chain configuration onto it: `->onQueue('billing')`, `->onConnection('redis')`, `->delay(now()->addMinutes(5))`, `->afterCommit()`. 4. When the `PendingDispatch` is destroyed — normally at the end of the statement — its destructor hands the job to the bus dispatcher. 5. The dispatcher checks `instanceof ShouldQueue`. If true, the job is **serialized** into a payload and pushed to the chosen connection and queue; if false, `handle()` runs immediately in the current process. Because the push happens in the destructor, storing the result (`$pending = GenerateInvoicePdf::dispatch($invoice);`) delays the push until that variable goes out of scope. The global `dispatch(new GenerateInvoicePdf($invoice))` helper behaves the same way. ## Where the job goes A **connection** is a backend defined in `config/queue.php`; a **queue** is a named pile of jobs on that connection. The Laravel 13 skeleton ships `QUEUE_CONNECTION=database` in `.env.example`, and its migrations already create the `jobs`, `job_batches` and `failed_jobs` tables, so a fresh app queues into a database table with no extra infrastructure. | Setting | Per dispatch | On the class (Laravel 13) | |---|---|---| | Queue name | `->onQueue('billing')` | `#[Queue('billing')]` | | Connection | `->onConnection('redis')` | `#[Connection('redis')]` | | Delay | `->delay(300)` or a `DateTimeInterface` | `#[Delay(300)]` (seconds) | A value set at dispatch time overrides the class attribute. The attributes live in `Illuminate\Queue\Attributes`. A job sent to `billing` only runs if some worker listens on `billing`; which queues a worker drains, and in what order, is worker configuration. ## Common first-job mistakes - **No `ShouldQueue`.** The class still has `dispatch()`, but the dispatcher runs `handle()` inline, and the request waits for it. - **No worker.** On the database connection, jobs accumulate as rows in `jobs` until a `php artisan queue:work` process (or `php artisan dev`, which starts one) consumes them. - **Unserializable constructor data.** The payload is JSON with a serialized command inside; open resources cannot be serialized, and the docs say raw binary data should be `base64_encode`d first. - **Expecting a return value.** `dispatch()` returns the `PendingDispatch`, not the result of `handle()`; the work happens later, in another process. - **Tests that seem synchronous.** The skeleton's `phpunit.xml` sets `QUEUE_CONNECTION=sync`, so jobs run inline during tests and a delay has no effect there. ## What the worker does with it A worker process started with `php artisan queue:work` polls its connection, reserves the next available job and rebuilds it: - it unserializes the command from the payload, and `SerializesModels` re-fetches any Eloquent models by key; - the service container calls `handle()`, resolving every type-hinted parameter, so the job can ask for a mailer, an HTTP client or your own service class; - when `handle()` returns, the job is deleted from the queue; - when it throws, the worker either releases it for another attempt or records it in `failed_jobs`, depending on how many tries the job and the worker allow. Because the worker is a long-lived process that booted the application once, a code change is only picked up after the worker restarts — which is why deployments restart queue workers. ## Why the split matters Queuing moves slow or failure-prone work — PDF rendering, third-party API calls, mail — out of the request, so the response returns quickly and the work can be retried by the worker. The price is that the job runs later, in a different process, against whatever the database holds at that moment. Everything else in this area — the transaction timing, model serialization, uniqueness — follows from that one fact.

  • In Laravel, what happens if you keep the result of dispatch() in a variable?
    `dispatch()` returns a `PendingDispatch`, and the job is handed to the dispatcher in that object's destructor. Assigned to a variable, the push waits until the variable is unset or goes out of scope, for example at the end of the method. That is usually harmless, but it surprises code that expects the job to be queued on that line.
  • How do you route one Laravel job class to a specific queue without repeating onQueue() at every call site?
    In Laravel 13, put `#[Queue('billing')]` (and `#[Connection('redis')]` if needed) on the job class; the dispatcher reads the attribute when it pushes. A call-site `->onQueue()` still wins. Laravel 13 also adds `Queue::route(Job::class, connection: ..., queue: ...)` for routing from a service provider. Either way, a worker must be listening on that queue.

saying these in an interview costs you the question

  • A job without ShouldQueue is still queued because it uses the Queueable trait
  • dispatch() runs handle() and returns its result to the caller
  • A new Laravel 13 app uses the sync queue connection by default
  • Dispatched jobs run even when no queue worker is running
  • Services should be passed into the job constructor rather than injected into handle()