Laravel recently introduced a new defer helper function, allowing you to defer the execution of a closure until a response is sent to the user — helping make these responses feel much faster. Let's dive into how it works.
Here's a basic example of how the new defer
helper is used:
Route::get('/', function () {
defer(function () {
// execute something
});
return response('This is the response');
});
If, and only if the response is successful (not a 4xx or 5xx status code) then the closure provided to the defer
helper will be executed after the response.
Here's a slightly better example to see how this works:
Route::get('/', function () {
defer(function () {
sleep(3);
\Log::info('Some log');
});
return response('This is a response');
});
In this example, when hitting the /
route, users will instantly see the This is a response
response. But, 3 seconds after the response has been sent successfully, Laravel will log Some log
. This demonstrates that any slightly longer running task you need to execute that doesn't need to be sent with the response can use the defer helper without pushing this to a queue.
Here's a simplified example directly from the Codecourse codebase.
On the course watch page, several tasks need to be run to set the status for a user before they land on the page. However, these tasks (like marking a course in progress for a user) are not required to be run before the user actually sees the page. Perfect for being deferred!
class WatchController
{
public function __construct(
protected AddCourseToProgress $addCourseToProgress
) { }
public function __invoke(Course $course)
{
// Mark the course as 'in progress' for the user
defer(fn () => $this->addCourseToProgress->handle(auth()->user(), $course));
return inertia()->render('Watch', [
'course' => CourseResource::make($course),
// some other things
]);
}
}
This means that the response hits the user immediately, and the course gets added to the user's list of in-progress courses behind the scenes. AddCourseToProgress
only marks this in the database, so it's not a long-running task, but it shaves off a few milliseconds.
At first, it feels like this deferred invoking is being pushed to some queue — but it's not.
Along with the defer
helper, Laravel includes some InvokeDeferredCallbacks
middleware with a terminate
method. This is responsible for invoking any deferred callbacks after a successful response has been sent to the user. Here's what it looks like:
class InvokeDeferredCallbacks
{
//...
public function terminate(Request $request, Response $response)
{
Container::getInstance()
->make(DeferredCallbackCollection::class)
->invokeWhen(fn ($callback) => $response->getStatusCode() < 400 || $callback->always);
}
}
After the response has been sent to the user, this terminate
method uses a DeferredCallbackCollection
to invoke any callbacks provided where defer
has been used. As you can see from the example above, this is conditional on the status code being less than 400 (e.g. a 2xx or 3xx status code), which we already mentioned.
terminate
method after sending the response?It seems strange since the response has already been sent, and the request has been closed. But, with Laravel using FastCGI, the PHP process can stay open after the response has been sent to the user. That's where Laravel swoops in and has the chance to invoke terminate
after the response has been sent.
Think back to our sleep
and log
example at the start of the article. Here's a really basic timeline:
/
.defer
in the DeferredCallbackCollection
.terminate
.defer
helper calls.This means that behind the scenes, the code within the closures provided to invoke
continues to run in the background within a PHP process. Kinda like a queue, but not quite.
The defer
helper is great for when a queue doesn't make sense. I can't provide a hard and fast list of when you shouldn't use defer
, but I'd recommend only using it for smaller tasks. For example, you wouldn't use it to defer the process of encoding a video or importing lines from a CSV — that's best left to queues.
Like anything, you'll get a feel for which option feels right as you build your applications.
I hope this helps clear up the defer
function and what's happening behind the scenes. Happy deferring!