Laravel 11 quietly introduced one of the most practical helpers in years: defer().
It lets you run code after the HTTP response has been sent — no queue worker, no Redis, no Supervisor config. Just:
defer(fn () => Metrics::recordOrder($order));
The user gets the response instantly. The closure runs after. Zero extra infrastructure.
But a lot of developers are either ignoring it or misusing it. So here's the honest breakdown.
The One-Line Rule
-
defer()→ "I'm okay if this is lost on a crash" - Queue Job → "This must succeed, and retry if it fails"
What defer() actually does
Runs a closure in the same PHP process, after the HTTP response is sent.
use function Illuminate\Support\defer;
Route::post('/orders', function (Request $request) {
$order = Order::create($request->validated());
defer(fn () => Metrics::recordOrder($order)); // fires AFTER response
return response()->json($order, 201); // returned immediately
});
Key behaviours:
- Skips execution if the response is a
4xxor5xx(use->always()to override) - Can be named and cancelled:
defer(fn () => ..., 'name')+defer()->forget('name') - Doesn't survive a server crash — not persisted anywhere
Version support: Laravel 11+ only. Not available in Laravel 10 or earlier.
Quick Comparison Table
| Feature | Queue Job | defer() |
|---|---|---|
| Needs queue worker | ✅ Yes | ❌ No |
| Needs Redis / DB driver | ✅ Yes | ❌ No |
| Persisted to storage | ✅ Yes | ❌ No |
| Retries on failure | ✅ Yes | ❌ No |
| Delayed execution | ✅ Yes | ❌ No |
| Monitoring (Horizon) | ✅ Yes | ❌ No |
| Zero setup | ❌ No | ✅ Yes |
| Runs after response | separate process | ✅ same process |
Use defer() for:
- Recording analytics after an order
- Incrementing page view / "last seen" counters
- Busting a cache key after a resource is created
- Non-critical internal Slack notifications
- Lightweight audit trail writes
Use Queue Jobs for:
- Transactional emails (must not be lost)
- Payment webhooks and API calls (must retry)
- Image/file processing (CPU heavy)
- Delayed tasks ("send reminder in 24 hours")
- Anything monitored via Horizon or Telescope
⚠️ The Swoole Gotcha
If you use Swoole or FrankenPHP, always import Laravel's defer() explicitly:
// WRONG on Swoole — calls Swoole's defer(), not Laravel's
defer(fn () => Metrics::record($data));
// CORRECT
use function Illuminate\Support\defer;
defer(fn () => Metrics::record($data));
Swoole has its own global defer(). The conflict is silent and will cost you hours.
Testing Deferred Functions
public function test_order_records_metrics(): void
{
$this->withoutDefer(); // runs deferred closures immediately
Metrics::shouldReceive('recordOrder')->once();
$this->postJson('/api/orders', ['product_id' => 1, 'qty' => 2])
->assertStatus(201);
}
Full post
For the full decision guide (including the 8-question decision matrix, Laravel 13 notes, and full code examples):
👉 pola5h.github.io/blog/laravel-queue-vs-defer/
What do you use defer() for in your projects? Drop it in the comments 👇
Top comments (0)