In Laravel, when does a closure passed to defer() run, when is it skipped, and when should the work be a queued job instead?
answer
- use function Illuminate\Support\defer
- runs after the response is sent
- skipped on 4xx/5xx unless always()
- named callbacks can be forgotten
- no retries, no durability
basics
~20 sdefer() runs a closure after the response is sent, or after a command or queued job ends, and only on success (status below 400) unless always() is chained. Nothing retries it, so work that must happen belongs in a queued job.
solid answer
~40 s`Illuminate\Support\defer()` (imported with `use function`) stores a `DeferredCallback` for the current request, command or job. In a web request the global `InvokeDeferredCallbacks` middleware runs the callbacks in its `terminate()` step, after the response has gone out — but only when the status is below 400, unless you chain `->always()`. In Artisan they run when the command exits with 0, and in a queue worker when the job attempt succeeds. Each runs through `rescue()`, so an exception is reported and swallowed. `defer($cb, 'name')` plus `defer()->forget('name')` cancels one. The work still occupies the same PHP process and has no retry or persistence, so use `defer()` for short, loss-tolerant tasks — metrics, cache warming — and a queued job for anything that must complete, needs retries or runs long.
code
php · 26 lines<?php
use App\Jobs\SendShipmentConfirmation;
use App\Services\Metrics;
use App\Services\OrderService;
use Illuminate\Http\Request;
use function Illuminate\Support\defer;
class OrderController
{
public function __construct(private OrderService $orders) {}
public function store(Request $request)
{
$order = $this->orders->place($request);
// Must happen, may need retries: queue it.
SendShipmentConfirmation::dispatch($order);
// Nice to have, cheap, loss-tolerant: defer it.
defer(fn () => Metrics::reportOrder($order), 'reportOrder');
return redirect()->route('orders.show', $order);
}
}go deeper
Remember that defer() runs a closure after the response is sent, and that it is skipped when the response is an error unless always() is used.
Explain the three triggers — middleware terminate, command finished, job attempted — the status and exit-code conditions, naming with forget(), and rescue() swallowing exceptions.
Show you choose deliberately: defer() only for short, loss-tolerant work, queued jobs for durable or retryable work, and awareness that deferred work still holds the serving worker.
Set team rules for post-response work: what may be lost, what must be durable, and how failures of each kind are observed.
## What defer() does `defer()` is a namespaced function, `Illuminate\Support\defer`, imported with `use function Illuminate\Support\defer;`; Laravel also defines a global `defer()` helper that forwards to it, but only when no other global `defer` function exists. Its signature is `defer(?callable $callback = null, ?string $name = null, bool $always = false)`: - with a callback it wraps it in an `Illuminate\Support\Defer\DeferredCallback` and adds it to a **scoped** `DeferredCallbackCollection` for the current request, command or job, returning the `DeferredCallback`; - with no arguments it returns the collection itself, which is how `defer()->forget('name')` works. The callback does not run where you call `defer()`. It waits until the surrounding unit of work has finished. ## When deferred callbacks run | Context | What triggers them | Condition | |---|---|---| | HTTP request | `terminate()` of the global `InvokeDeferredCallbacks` middleware, after the response is sent | status below 400, or `always()` | | Artisan command | the `CommandFinished` event | exit code 0, or `always()` | | queued job | the `JobAttempted` event | the attempt succeeded, or `always()` | Jobs on the `sync` and `deferred` queue connections are an exception: the listener ignores their `JobAttempted` events, so callbacks deferred inside such a job stay in the collection and run with the surrounding request or command instead. So `defer(fn () => Metrics::reportOrder($order))` in a controller that ends with a validation error (422) or a server error (500) never runs. `->always()` — or the third argument — makes it run regardless of the outcome. ## Failure and cancellation - Each callback runs through `rescue()`: an exception is **reported** through the exception handler and then swallowed, and the remaining callbacks still run. - Nothing retries a failed callback, and nothing stores it. If the PHP process is killed or recycled before the callback runs, the work is gone. - Naming a callback, `defer($callback, 'syncCrm')` or `->name('syncCrm')`, lets later code cancel it with `defer()->forget('syncCrm')`. - If two callbacks share a name, only the one registered last runs. ## Concurrency::defer() `Concurrency::defer([...])` combines deferral with parallel execution. It also waits until after the response; with the default `process` driver it then launches each closure as a **background PHP process** and does not collect output, results or failures. With the `sync` driver the closures simply run one by one in the current process after the response. ## defer() versus a queued job | Concern | `defer()` | queued job | |---|---|---| | infrastructure | none beyond the app | a queue connection and running workers | | durability | lost if the process dies | stored until processed | | retries | none | configurable per job | | where it runs | the same PHP process that served the request | a separate worker process | | start delay | immediately after the response | when a worker picks it up | | visibility | only the reported exception | failed-job records and queue tooling | The key point is that `defer()` moves work **after** the response, not **off** the server process. Under PHP-FPM the worker that served the request stays busy until the deferred work finishes, so a slow deferred task still ties up capacity even though the user has their page. ## A practical rule 1. **Use `defer()`** for short, idempotent, loss-tolerant work: recording analytics, warming a cache entry, pinging a webhook whose failure you can live with. 2. **Use a queued job** when the work must happen (sending an invoice, charging a card), may need retries, takes more than a moment, or should run on separate machines. 3. **Use `Concurrency::defer()`** when several fire-and-forget tasks should run in parallel after the response and you accept that failures are not collected. ## Pitfalls - Deferring critical work and discovering it silently never ran because the response was a redirect-with-error or a 500. - Assuming the user-facing worker is free again the moment the response is sent. - Calling the global `defer()` on a server with the Swoole extension, which defines its own global `defer`; Laravel then skips its global helper, so the documentation tells you to import `use function Illuminate\Support\defer;` explicitly.
- What happens to a deferred callback if the controller ends by throwing an exception that renders a 500?It does not run. The middleware invokes deferred callbacks only when the response status is below 400, unless the callback was marked `always()`. The same rule uses the exit code for Artisan commands and the attempt outcome for queued jobs.
- If a deferred callback throws, does the user see an error?No. The response has already been sent, and each callback runs inside `rescue()`, which reports the exception to the exception handler and continues with the next callback. The failure is visible only in your error reporting.
- Does defer() free the PHP-FPM worker sooner than doing the work inline?No. The user receives the response sooner, but the same worker runs the deferred callback afterwards and is not available for another request until it finishes. Only a queued job moves the work to a different process.
A waiter who serves your dish and then, before taking the next table, clears your plates: you are already eating, but the waiter is not free yet, and if the kitchen catches fire the plates simply never get cleared.
saying these in an interview costs you the question
- defer() pushes the closure onto the queue, so a worker runs it later.
- Deferred callbacks run even when the response is a 500, unless you cancel them.
- A failing deferred callback is retried automatically.
- Once the response is sent, the PHP worker is free for the next request.
- defer() is a good fit for charging a card after checkout.