In Laravel 13, what runs during the kernel's terminate() after the response is sent, in what order, and what are the pitfalls of work placed there?
answer
- handleRequest: handle, send, terminate
- Terminating event first
- terminable middleware, then terminating() callbacks
- whenRequestLifecycleIsLongerThan handlers last
- FastCGI decides whether the client waits
basics
~20 sAfter send(), the kernel's terminate() dispatches the Terminating event, calls terminate() on terminable middleware, runs app()->terminating() callbacks (including afterResponse jobs), then runs slow-request handlers. The work cannot change the response and still holds the worker process.
solid answer
~40 s`Application::handleRequest()` calls `$kernel->terminate($request, $response)` after `send()`. In Laravel 13 the HTTP kernel then: dispatches the `Illuminate\Foundation\Events\Terminating` event; calls `terminate($request, $response)` on each terminable route and global middleware (resolving a fresh instance unless the middleware is bound as a singleton); calls `Application::terminate()`, which runs every callback registered with `app()->terminating()` in order, including ones added while it runs and jobs dispatched with `afterResponse()`; and finally runs handlers registered with `whenRequestLifecycleIsLongerThan()` if the request exceeded their threshold. Pitfalls: nothing there can alter the response; under PHP-FPM the client already has its response but the worker process is busy until the work finishes; without FastCGI's early finish the client may wait; and an exception there cannot become an error page.
code
php · 22 lines<?php
namespace App\Providers;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->app->make(Kernel::class)->whenRequestLifecycleIsLongerThan(
2000,
fn ($startedAt, Request $request, $response) => Log::warning('Slow checkout', [
'path' => $request->path(),
'status' => $response->getStatusCode(),
]),
);
}
}go deeper
Know that terminate() runs after the response is sent, so it is for background-style work, not for changing the response.
List what terminate() runs in order, and explain how afterResponse() jobs ride on terminating() callbacks.
Weigh post-response callbacks against queued jobs, watch worker capacity under PHP-FPM, and handle terminable middleware's fresh-instance behaviour.
Set guidance on what post-response work is acceptable in-process across services, given capacity limits and the lack of retries.
## Where terminate fits `Application::handleRequest()` ends with three calls: the kernel's `handle()`, the response's `send()`, then the kernel's `terminate($request, $response)`. `terminate()` is the **post-response phase**: the place for work the user should not wait for, such as writing an audit record or flushing metrics. ## What the HTTP kernel's `terminate()` does, in order 1. **Dispatches `Terminating`** (`Illuminate\Foundation\Events\Terminating`), an event any listener can hook. 2. **Calls terminable middleware.** It gathers the route middleware and the global middleware and, for each one given as a class name, resolves it from the container and calls `terminate($request, $response)` if the method exists. A middleware not bound as a singleton is a **new instance**, so properties set in `handle()` are gone. 3. **Calls `Application::terminate()`**, which runs every callback registered with `app()->terminating($callback)`, in registration order, resolving their parameters from the container. The loop re-checks the list length, so a callback may register another one and it still runs. 4. **Runs slow-request handlers** registered with the kernel's `whenRequestLifecycleIsLongerThan($threshold, $handler)`: if the time since `requestStartedAt` exceeds the threshold (milliseconds, a `CarbonInterval`, or a `DateTimeInterface`), the handler receives the start time, request and response. Jobs dispatched with `afterResponse()` ride on step 3: the bus registers them as a `terminating` callback. ## Pitfalls | Pitfall | Why | |---|---| | Changing headers or the body | The response was already sent; see `send()` above | | Assuming the user never waits | The middleware docs promise post-response timing under FastCGI; other servers may hold the connection until terminate work ends | | Long work in the callback | Under PHP-FPM the worker process stays busy, reducing capacity for new requests | | Errors in terminating code | The response has gone, so there is no error page; rely on reporting and logs | | State from `handle()` in middleware | `terminate()` gets a fresh instance unless the middleware is a singleton | | Heavy or retryable work | A queued job, run by a worker with retries, is safer than an in-process callback | ## A ticket-resale example After a buyer reserves seats, the controller records an analytics event and warms the cache for the event page. Neither should delay the reservation response, so both go into `app()->terminating(...)`. The team also registers a `whenRequestLifecycleIsLongerThan(2000, ...)` handler in `AppServiceProvider::boot()` to log checkouts slower than two seconds. The seat reservation itself stays inside the request, because the buyer needs its result. ## Choosing the right tool - **Inside the request**: anything the response depends on. - **`terminating()` callbacks or `afterResponse()` jobs**: small, best-effort work that may fail silently. - **Queued jobs**: anything that must be retried or may take long. - **Terminable middleware**: post-response work tied to a middleware's concern, such as a session or rate-limit record; how to write one is a middleware topic. ## In HTTP tests Laravel's HTTP testing helpers call the kernel's `handle()` and then `terminate()` directly, without `send()`. So in a feature test, `terminating()` callbacks and `afterResponse()` jobs have already run by the time the test inspects the response. That is convenient for asserting their effects, but it hides the timing difference a real server has, where the user already has the response while that work runs. ## Long-lived workers Under PHP-FPM, every request builds a new application, so terminating callbacks cannot pile up across requests. Where one application instance serves many requests, the same hooks behave differently; that belongs to the worker-server topic.
- Why can a terminable middleware lose data it stored in handle()?The kernel's `terminateMiddleware()` resolves each middleware class from the container again before calling `terminate()`. Unless the middleware is registered as a singleton, that is a new object, so properties set during `handle()` on the first instance are not there. Bind it as a singleton, or pass the data through the request or response.
- Can a callback registered inside another terminating() callback still run?Yes. `Application::terminate()` walks the callback list with an index and re-reads `count()` on each iteration, so a callback appended during the loop is reached before it ends. That is how work scheduled late in the request, such as an after-response job dispatched from a terminating callback, still runs.
saying these in an interview costs you the question
- terminating() callbacks run before the response is sent
- Work in terminate() never affects server capacity
- Terminable middleware always reuses the handle() instance
- An exception in a terminating callback shows the user an error page
- afterResponse() jobs go to the queue worker