TL;DR: Laravel 11 introduced defer() which runs code after the HTTP response is sent to the user. No queues, no job classes, no workers. Just wrap your fire-and-forget logic in defer() and your API becomes instantly faster.
I spent two days last year trying to figure out why our order API endpoint was taking 1.2 seconds to respond. The order itself was being created in about 80ms. So where was the rest of the time going?
Turns out we were sending a confirmation email, tracking an analytics event, syncing inventory with a third-party service, and clearing a cache key. All of that happened synchronously before the response was sent back to the user.
The user did not care about any of those things completing before they saw their order confirmation. They just wanted to know their order went through.
The Old Way: Queues for Everything
The typical advice is to push these tasks onto a queue. Create a job class, dispatch it, run a queue worker, set up monitoring, handle failed jobs. For a large application with complex background processing that makes sense.
But for simple fire-and-forget tasks like sending an email or tracking an event? That is a lot of infrastructure for something that should be simple.
We had 14 different job classes in our application. Eight of them were single-method classes that just did one small thing. Each one had its own file, its own test, and its own entry in the failed jobs table. It felt like overkill.
Enter defer()
Laravel 11 added defer() and it changed how I think about background tasks. Here is how it works:
Route::post('/order', function () {
$order = Order::create($data);
defer(fn() => Mail::send(new OrderConfirmation($order)));
defer(fn() => Analytics::track('order_placed', $order));
defer(fn() => InventorySync::push($order));
defer(fn() => Cache::forget("user:{$order->user_id}:cart"));
return response()->json($order);
});
The response goes back to the user immediately after the order is created. Then Laravel runs all the deferred callbacks after the response is sent. The user never waits for them.
No job classes. No queue workers. No Redis or database queue driver. Just a closure that runs after the response.
When To Use defer() vs Queues
This is the part most articles get wrong. They either say "use defer for everything" or "always use queues." The reality is more nuanced.
Use defer() when the task is simple and does not need retry logic. Sending a notification email, tracking an analytics event, clearing a cache, logging an activity. If it fails you do not need to retry it automatically.
Use queues when the task is complex or needs reliability guarantees. Processing a payment, generating a large PDF, syncing thousands of records with an external API. If it fails you need to know about it and retry it.
I made the mistake of using defer() for a webhook delivery early on. The webhook target was unreliable and about 10% of deliveries failed silently. There was no retry mechanism, no failed job record, no way to know it happened. I moved that back to a queue with 3 retries and the problem was solved.
Real Numbers
After refactoring our order endpoint to use defer() for non-critical tasks:
- Response time dropped from 1.2 seconds to 280ms
- User-perceived performance improved dramatically
- We deleted 8 single-method job classes
- Queue worker load decreased because fewer jobs were being dispatched
The email still sends. The analytics event still tracks. The cache still clears. The user just does not wait for any of it.
A Pattern I Use in Every Project
I created a simple middleware that defers common request-level tasks:
class DeferCommonTasks
{
public function handle($request, Closure $next)
{
$response = $next($request);
defer(fn() => ActivityLog::record($request));
defer(fn() => MetricsCollector::trackRequest($request, $response));
return $response;
}
}
Every request automatically logs activity and tracks metrics without adding any latency to the response. The middleware runs once and every endpoint benefits from it.
What I Would Do Differently
If I started over I would establish a clear rule from day one: if the task does not need retry logic and takes less than 5 seconds, use defer(). If it needs retries or takes longer, use a queue. Having that rule early would have prevented the 14 unnecessary job classes we ended up with.
I also would not use defer() inside database transactions. If the transaction rolls back the deferred callback still runs because it executes after the response. This caused a bug where we were sending order confirmation emails for orders that failed to save. I learned that the hard way.
The Bottom Line
defer() is not a replacement for queues. It is a replacement for the dozens of tiny job classes that exist only because you needed something to run "after" the response.
One line of code. No infrastructure changes. Measurably faster responses.
If you are on Laravel 11 or later and you are not using defer() yet you are probably making your users wait for things they do not need to wait for.
What is the simplest performance win you have found in your Laravel applications?
Top comments (0)