In Laravel, how do dispatch(), dispatchSync(), dispatchAfterResponse() and dispatchIf() differ in where and when a job's handle() runs?
answer
- queue, now, or after the reply
- dispatchSync uses the sync connection
- terminating callback, same PHP process
- dispatchIf returns a Fluent when false
- deferred and background connections
basics
~20 sdispatch() 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.
solid answer
~50 s`dispatch()` pushes a `ShouldQueue` job to its connection and returns immediately; a worker runs it later with retries. `dispatchSync()` routes the same job through the `sync` connection, so `handle()` runs before the call returns and an exception propagates to the caller — useful in commands or when you need the work finished now. `dispatchAfterResponse()` registers the job on the application's terminating callbacks: the response goes out first, then the job runs synchronously in the same PHP process. It needs no worker, but it is not durable, it is not retried, and it keeps that PHP-FPM worker busy. In Laravel 13 the docs describe the same idea as the `deferred` connection, plus `background`, which spawns a separate PHP process. `dispatchIf($cond, ...)` and `dispatchUnless()` wrap the conditional and return a `Fluent` instead of a `PendingDispatch` when nothing is dispatched.
code
php · 16 lines<?php
use App\Jobs\ProcessPodcast;
use App\Jobs\RecordPlay;
// Worker, later, with retries
ProcessPodcast::dispatch($podcast);
// Now, in this process; exceptions reach the caller
ProcessPodcast::dispatchSync($podcast);
// This process, after the response is sent; no retries
RecordPlay::dispatchAfterResponse($podcast, $request->user());
// Only when the flag is true
ProcessPodcast::dispatchIf($podcast->isPublished(), $podcast);go deeper
Know the four method names and say in one line where each runs: worker, now, after the reply, or conditionally.
Explain the sync connection, the terminating callback behind dispatchAfterResponse(), and why only dispatch() gets worker retries.
Judge which work may run after the response and which needs a durable queue, citing lost work on deploys and FPM capacity.
Set a team rule for after-response work versus real queues, weighing latency against durability and the infrastructure a queue requires.
## Four entry points, three execution places Every job class that uses `Dispatchable` (the Laravel 13 `Queueable` trait includes it) gets a family of static methods. They differ in **where** `handle()` runs and **when**: | Method | Where `handle()` runs | When | Retried by a worker? | |---|---|---|---| | `dispatch()` | a queue worker process | later | yes, per the job's tries | | `dispatchSync()` | the current process | before the call returns | no | | `dispatchAfterResponse()` | the current process | after the response is sent | no | | `dispatchIf()` / `dispatchUnless()` | as `dispatch()` | only if the condition passes | as `dispatch()` | ## dispatch() and dispatchSync() `dispatch()` builds the job, wraps it in a `PendingDispatch` and, when that object is destroyed, hands it to the bus dispatcher. For a `ShouldQueue` job the dispatcher serializes it and pushes it to the configured connection; for a class without `ShouldQueue` it simply calls `handle()` now. `dispatchSync()` calls the dispatcher's `dispatchSync()`. For a queueable job it sets the connection to `sync` and pushes it there. The **sync driver** executes the job immediately in the current process: - the job still goes through a payload round-trip, so `SerializesModels` re-fetches models just as a worker would; - any delay is ignored, because the sync queue's `later()` just calls `push()`; - an exception is rethrown to the caller after the failure handling runs, so the calling code sees it; - there is no worker, so there are no worker-driven retries. Use it in an Artisan command or a seeder that must finish the work before continuing, or to run the same job class inline when the queue would add nothing. ## dispatchAfterResponse() `dispatchAfterResponse()` is `dispatch(...)->afterResponse()`. When the `PendingDispatch` is destroyed, the dispatcher registers a callback with the application's `terminating()` hook. The HTTP kernel sends the response, then runs its terminate phase, which calls `dispatchSync()` on the job. So: 1. the browser gets its response without waiting; 2. the job runs in the **same PHP process**, synchronously; 3. nothing is written to a queue, so a crash or a deploy at that moment loses the work; 4. the PHP-FPM worker stays busy until the job finishes, reducing capacity for other requests; 5. failures are not retried by any worker. It suits short, best-effort work such as recording an analytics ping or warming one cache key. Anything that must happen, or that takes seconds, belongs on a real queue. In Laravel 13 the queue docs present this pattern as **deferred dispatching**: `RecordDelivery::dispatch($order)->onConnection('deferred')`. The `deferred` connection also processes the job in the current process after the response. The `background` connection instead spawns a separate PHP process, so the PHP-FPM worker is freed sooner. Both connections ship in the skeleton's `config/queue.php`. ## dispatchIf() and dispatchUnless() `ProcessPodcast::dispatchIf($accountActive, $podcast)` is sugar for an `if` around `dispatch()`. Two details are worth knowing: - the condition may be a boolean or a closure; a closure receives the constructed job, so the job object is built before the check; - when the condition fails, the method returns an `Illuminate\Support\Fluent` rather than a `PendingDispatch`, so chaining `->onQueue()` on it configures nothing. ## What each one costs The four methods trade the same three things against each other: **latency** for the caller, **durability** of the work and **capacity** of the process that runs it. - `dispatch()` adds a queue write to the request (a row insert on the default database connection) and needs a worker running somewhere, but the work survives a web-server restart and is retried. - `dispatchSync()` adds the full run time of `handle()` to the caller, and a failure surfaces as an exception in the caller's code. - `dispatchAfterResponse()` adds nothing the user waits for, but the PHP-FPM child stays busy, and a long job there reduces how many requests the pool can serve at once. - A `sync` default connection makes every `dispatch()` behave like `dispatchSync()`; that is convenient in tests and a surprise in production if someone forgets to change `QUEUE_CONNECTION`. ## Choosing between them - The work must happen and may be slow or flaky: `dispatch()` to a real connection. - The caller needs the result now, or there is no HTTP response to protect: `dispatchSync()`. - The work is short, optional and should not delay the reply: `dispatchAfterResponse()` or the `deferred` connection. - The dispatch depends on a flag: `dispatchIf()` / `dispatchUnless()`. A frequent review comment is `dispatchAfterResponse()` used for payment webhooks or emails: it removes the latency from the request but also removes retries and durability, which is exactly what those tasks need.
- Why is dispatchAfterResponse() a poor fit for sending an order-confirmation email in Laravel?It runs the job in the web process after the reply, with no queue behind it. If the mail server times out, nothing retries it; if the process is killed during a deploy, the email is lost; and the PHP-FPM worker is tied up while the mail is sent. A queued `dispatch()` gives durability, retries and a failed-jobs record.
- Does dispatchSync() on a ShouldQueue job skip SerializesModels in Laravel?No. `dispatchSync()` sends the job through the `sync` connection, which still builds a payload and resolves it. Eloquent models are stored as identifiers and re-fetched before `handle()` runs, exactly as on a worker, so unsaved changes on a model passed in are not visible to the job.
saying these in an interview costs you the question
- dispatchAfterResponse() pushes the job to the queue once the response is sent
- dispatchSync() waits for a worker to finish the job and returns its result
- Work run after the response is retried automatically if it throws
- dispatchIf() with a false condition still returns a PendingDispatch you can configure
- The background connection runs the job inside the same PHP-FPM process