Terminable middleware — terminate() after response is sent
Concept
Terminable middleware can execute code AFTER the HTTP response has been sent to the client. This is enabled by the terminate() method on middleware classes.
How it works: After Laravel sends the response, it calls terminate(Request $request, Response $response) on all middleware that implement it. The client has already received the response — this code doesn't affect response time.
Use cases:
- Request logging: Log the final response code, headers, and response body after the request completes.
- Analytics: Record request metrics without slowing the response.
- Session flushing: Write session data to storage after response delivery (this is what Laravel's
StartSessionmiddleware does in itsterminate()method). - Cleanup: Release resources that were held during the request.
Important: terminate() is only called if the server handles shutdown properly (PHP-FPM with fastcgi_finish_request() called, or Octane). In some configurations, terminate() blocks the next request. Verify your server sends the response before terminate() runs.
PHP-FPM and fastcgi_finish_request(): PHP-FPM automatically calls fastcgi_finish_request() before cleanup functions, allowing terminate middleware to run after the response. Laravel's App\Http\Kernel::terminate() calls this.
Octane and terminable middleware: In Octane, terminate() runs between requests (after response sent, before next request). State from terminate() should be cleaned up carefully.
Code Example
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
// Terminable middleware — logs after response is sent
class RequestLogger
{
private float $startTime;
public function handle(Request $request, Closure $next): Response
{
$this->startTime = microtime(true);
return $next($request);
}
// Called AFTER response is sent to client
public function terminate(Request $request, Response $response): void
{
$duration = round((microtime(true) - $this->startTime) * 1000, 2);
logger()->info('Request completed', [
'method' => $request->method(),
'path' => $request->path(),
'status' => $response->getStatusCode(),
'duration' => $duration . 'ms',
'ip' => $request->ip(),
'user_id' => auth()->id(),
]);
}
}
// Real example: StartSession middleware (Laravel core)
// This is essentially what Laravel does:
class StartSession
{
public function handle(Request $request, Closure $next): Response
{
$this->startSession($request); // start session
$response = $next($request);
$this->storeCurrentUrl($request); // save current URL in session
$this->collectGarbage(); // GC expired sessions
return $this->addCookieToResponse($response); // add session cookie
}
public function terminate(Request $request, Response $response): void
{
// Write session data to storage AFTER response is sent
// This avoids blocking the response on session writes
if ($this->sessionHandled) {
$this->manager->driver()->save();
}
}
}
// Usage in routes — terminable middleware works just like normal middleware
Route::middleware(RequestLogger::class)->group(function() {
Route::apiResource('orders', OrderController::class);
});