Dive into Laravel's new defer() helper

September 17th, 2024 • 3 minutes read time

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.

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:

  1. User hits /.
  2. Laravel stores the closure from defer in the DeferredCallbackCollection.
  3. The response is sent to the user.
  4. (The PHP process is still open).
  5. Laravel invokes terminate.
  6. Laravel runs each closure provided to any defer helper calls.
  7. PHP runs the closures in the background.

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!

If you found this article helpful, you'll love our practical screencasts.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first!