Laravel Magazine
Terminable Middleware: Running Code After the Response Has Already Been Sent

Terminable Middleware: Running Code After the Response Has Already Been Sent

Eric Van Johnson ·

A common but easy-to-miss inefficiency: doing "housekeeping" work — writing an audit log entry, recording a metric, cleaning up a temp file — inline in a middleware's handle() method, before the response goes out. The user's browser sits waiting for that work to finish even though none of it affects what gets rendered. Laravel's terminable middleware exists specifically to move that work to after the response has already been sent.

The normal middleware shape

namespace PhpArchitect\Http\Middleware;

class LogApiRequest
{
    public function handle($request, \Closure $next)
    {
        $response = $next($request);

        AuditLog::create([
            'path' => $request->path(),
            'status' => $response->status(),
            'user_id' => $request->user()?->id,
        ]);

        return $response;
    }
}

This works, but the AuditLog::create() call — a database write — happens before return $response completes, which means the client is still waiting on that database round trip even though it has zero bearing on the response body already sitting in $response.

Making it terminable

Add a terminate() method to the same middleware class:

namespace PhpArchitect\Http\Middleware;

class LogApiRequest
{
    public function handle($request, \Closure $next)
    {
        return $next($request);
    }

    public function terminate($request, $response): void
    {
        AuditLog::create([
            'path' => $request->path(),
            'status' => $response->status(),
            'user_id' => $request->user()?->id,
        ]);
    }
}

When a middleware defines terminate(), Laravel calls it after the response has been sent to the browser — under FPM, this happens via fastcgi_finish_request(), so the client connection closes and the user gets their page or JSON response immediately, while terminate() runs afterward on the same PHP process.

Register it like any other middleware

Nothing changes about registration — terminate() is picked up automatically if the class implements handle() and is registered as usual:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\PhpArchitect\Http\Middleware\LogApiRequest::class);
})

What terminable middleware is and isn't for

This is not a background job queue. The code in terminate() still runs synchronously within the same request's PHP process — it just runs after the response bytes are already on the wire. That makes it the right tool for short, fire-and-forget work: a log write, incrementing a metrics counter, deleting a temp upload. It's the wrong tool for anything that might fail and needs retries, anything slow enough to hold the PHP-FPM worker for a long time, or anything that genuinely needs queue guarantees like "this must eventually succeed even if the server crashes right now." For that class of work, dispatch a real queued job instead — terminable middleware and queued jobs solve adjacent but different problems, and reaching for the wrong one either adds needless queue infrastructure for a two-line log write, or silently drops work that should have had retry guarantees.

The other requirement: it needs FPM (or equivalent) to actually help

Terminable middleware's speed benefit relies on the server layer supporting closing the client connection before PHP execution fully ends — PHP-FPM does this via fastcgi_finish_request(). Running under php artisan serve in local development, or under certain non-FPM setups, terminate() still runs, but without the same "response already sent" behavior, so you won't see the timing benefit locally the way you will in production. Don't be surprised if a terminable middleware "feels" identical to a normal one on your machine — check the effect in a production-like environment before concluding it isn't working.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.