Request Lifecycle

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05. The middleware lists and the traced order below were produced by a new application, not copied from the documentation. Understanding the lifecycle answers most "where does this code go?" questions: which service provider method, which middleware, and what has (or has not) happened yet at each step. How PHP itself receives the request is on The PHP Request Model and Servers.

Diagram of the Laravel request lifecycle. The browser’s request reaches the web server and PHP (Nginx with PHP-FPM, FrankenPHP or artisan serve), then public/index.php, which checks maintenance mode, loads the Composer autoloader, requires bootstrap/app.php to create the application and calls handleRequest with the captured request, an Illuminate Request that extends Symfony HttpFoundation’s Request. The HTTP kernel runs its bootstrappers: load environment variables, load configuration, handle exceptions, register facades, call register() on every service provider and then boot() on every provider; under Octane everything up to here runs once per worker. The request then passes the global middleware (ValidatePathEncoding, InvokeDeferredCallbacks, TrustProxies, HandleCors, PreventRequestsDuringMaintenance, ValidatePostSize, TrimStrings, ConvertEmptyStringsToNull and appended middleware), the router, which matches the route or returns 404 or 405, the route middleware (the web group’s EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession, PreventRequestForgery and SubstituteBindings, then the route’s own such as auth and throttle) and the controller, which receives injected dependencies and validated form requests, queries Eloquent and returns a view, JSON, a redirect or a response. The response travels back out through the same middleware in reverse, the session is saved and the response sent, and afterwards terminable middleware, terminating callbacks and deferred functions run.

From index.php to the kernel

Every request enters through public/index.php. It checks whether storage/framework/maintenance.php exists (and if so serves the maintenance response without booting anything), loads Composer’s autoloader, and requires bootstrap/app.php, which creates the application: an instance of Illuminate\Foundation\Application, which is also the service container that every other object is resolved from. Then:

<?php
// public/index.php (the end of the file)
$app = require_once __DIR__.'/../bootstrap/app.php';

$app->handleRequest(Request::capture());

Request::capture() builds an Illuminate\Http\Request from the superglobals. It extends Symfony’s HttpFoundation\Request (the trace below prints the parent class), so the whole of Symfony’s request and response API (headers, cookies, files, content negotiation, streamed and binary file responses) is available, with Laravel’s helpers on top. handleRequest() passes the request to the HTTP kernel (Illuminate\Foundation\Http\Kernel); Artisan commands take the parallel path through the console kernel with handleCommand().

Bootstrapping and service providers

Before handling the request, the kernel runs its bootstrappers: it loads .env (unless the configuration is cached), loads the configuration, installs the error and exception handlers, registers the facades, and then loads the service providers. Providers are where the framework and the application set themselves up, in two passes:

  1. register() on every provider: bind things into the container, nothing else. Other services may not exist yet.

  2. boot() on every provider: everything is registered, so boot code can use any service (define gates, rate limiters, view composers, model observers, macros, event listeners).

Laravel itself has dozens of providers (database, queue, validation, routing, …); the application’s own are listed in bootstrap/providers.php. Deferred providers are only loaded when one of their services is first requested. More on Service Container, Providers and Facades.

Middleware

The request then passes through middleware, layers that can inspect or change the request, stop it, or change the response on the way back. There are two levels: global middleware run on every request, before routing; route middleware run only for the matched route, either as a group (web, api) or individually. A new Laravel 13.34 application’s HTTP kernel reports these stacks:

<?php
// php artisan tinker --execute="..."
$kernel = app(Illuminate\Contracts\Http\Kernel::class);
echo "global:\n  ", implode("\n  ", $kernel->getGlobalMiddleware()), "\n";
echo "web group:\n  ", implode("\n  ", $kernel->getMiddlewareGroups()['web']), "\n";
echo "api group:\n  ", implode("\n  ", $kernel->getMiddlewareGroups()['api']), "\n";
global:
 Illuminate\Http\Middleware\ValidatePathEncoding
 Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks
 Illuminate\Http\Middleware\TrustProxies
 Illuminate\Http\Middleware\HandleCors
 Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance
 Illuminate\Http\Middleware\ValidatePostSize
 Illuminate\Foundation\Http\Middleware\TrimStrings
 Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull
web group:
 Illuminate\Cookie\Middleware\EncryptCookies
 Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse
 Illuminate\Session\Middleware\StartSession
 Illuminate\View\Middleware\ShareErrorsFromSession
 Illuminate\Foundation\Http\Middleware\PreventRequestForgery
 Illuminate\Routing\Middleware\SubstituteBindings
api group:
 Illuminate\Routing\Middleware\SubstituteBindings

Read from top to bottom, this is what Laravel does before your code runs: it rejects malformed path encoding, prepares deferred callbacks, applies trusted-proxy headers, answers CORS preflights, returns 503 in maintenance mode, rejects bodies larger than post_max_size, trims strings and turns empty strings into null. For web routes it then decrypts cookies, starts the session, shares validation errors with the views, checks the CSRF token (PreventRequestForgery, which replaced VerifyCsrfToken in Laravel 13) and resolves route model bindings. Because the session is started by route middleware, global middleware cannot use the session, and the api group has no session or CSRF protection at all. Each middleware is described on Middleware.

Routing and the controller

After the global middleware, the router matches the method and path against the registered routes (404 when nothing matches, 405 when only the method is wrong), runs the matched route’s middleware, and calls the action: a controller method or a closure. The container resolves the controller and injects its constructor and method dependencies; form requests are validated and authorized at this point, before the method body runs. Whatever the action returns (a view, an array, a model, a redirect, a Response) is converted into a response object.

The way back and after the response

The response travels back out through the route middleware and then the global middleware in reverse order, giving each one the chance to modify it (adding headers, writing the session, queuing cookies). Then $response->send() sends it to the client. Work can still happen after that:

  • terminable middleware: a terminate($request, $response) method on a middleware runs after the response has been sent (with PHP-FPM, after fastcgi_finish_request());

  • app()->terminating(...) callbacks;

  • deferred functions: Illuminate\Support\defer(fn () => …) runs a closure after a successful response (not after 4xx or 5xx, unless ->always()), a lightweight alternative to a queued job.

The order, traced in a running application with a Trace::log() helper called from the provider, a global middleware with terminate(), a route middleware, the route action and a terminating callback:

<?php
// bootstrap/app.php: register the two tracing middleware
return Application::configure(basePath: dirname(__DIR__))
    // ... withRouting() and withExceptions() unchanged ...
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->append(\App\Http\Middleware\TraceGlobal::class);
        $middleware->alias(['trace.route' => \App\Http\Middleware\TraceRoute::class]);
    })
    ->create();
<?php
// app/Http/Middleware/TraceGlobal.php (TraceRoute logs steps 4 and 6 the same way)
namespace App\Http\Middleware;

use App\Support\Trace;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class TraceGlobal
{
    public function handle(Request $request, Closure $next): Response
    {
        Trace::log('3. global middleware, before');
        $response = $next($request);
        Trace::log('7. global middleware, after');
        return $response;
    }

    public function terminate(Request $request, Response $response): void
    {
        Trace::log('8. global middleware terminate()');
    }
}
<?php
// app/Providers/AppServiceProvider.php
namespace App\Providers;

use App\Support\Trace;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        Trace::log('1. AppServiceProvider::register()');
    }

    public function boot(): void
    {
        Trace::log('2. AppServiceProvider::boot()');
        $this->app->terminating(fn () => Trace::log('9. terminating callback (after the response was sent)'));
    }
}
<?php
// routes/web.php
Route::get('/trace', function (Illuminate\Http\Request $request) {
    App\Support\Trace::log('5. route action, request is a ' . get_parent_class($request));
    return 'ok';
})->middleware('trace.route')->name('trace');
$ curl localhost:8000/trace && cat storage/logs/trace.log
ok
1. AppServiceProvider::register()
2. AppServiceProvider::boot()
3. global middleware, before
4. route middleware, before (route: trace)
5. route action, request is a Symfony\Component\HttpFoundation\Request
6. route middleware, after: status 200
7. global middleware, after
8. global middleware terminate()
9. terminating callback (after the response was sent)

With php artisan serve, the log also showed one extra register()/boot() pair before the request’s: the serve command itself boots the application once in the CLI before starting the server.

When the action fails

The same trace answers what happens when the action does not return normally. A second route registers two deferred callbacks and then succeeds, aborts with a 403 or throws, and bootstrap/app.php reports exceptions to the trace too:

<?php
// routes/web.php
Route::get('/trace/{outcome}', function (string $outcome) {
    App\Support\Trace::log("5. route action ({$outcome})");
    Illuminate\Support\defer(fn () => App\Support\Trace::log('10. defer(), only after a successful response'));
    Illuminate\Support\defer(fn () => App\Support\Trace::log('10. defer()->always()'))->always();

    return match ($outcome) {
        'ok' => 'ok',
        'denied' => abort(403),
        'broken' => throw new RuntimeException('catalogue offline'),
    };
})->middleware('trace.route')->name('trace.outcome');

// bootstrap/app.php, in withExceptions()
$exceptions->report(fn (Throwable $e) => \App\Support\Trace::log('5a. exception reported: '.$e->getMessage()));
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/trace/ok; cat storage/logs/trace.log
200
1. AppServiceProvider::register()
2. AppServiceProvider::boot()
3. global middleware, before
4. route middleware, before (route: trace.outcome)
5. route action (ok)
6. route middleware, after: status 200
7. global middleware, after
10. defer(), only after a successful response
10. defer()->always()
8. global middleware terminate()
9. terminating callback (after the response was sent)
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/trace/broken; cat storage/logs/trace.log
500
1. AppServiceProvider::register()
2. AppServiceProvider::boot()
3. global middleware, before
4. route middleware, before (route: trace.outcome)
5. route action (broken)
5a. exception reported: catalogue offline
6. route middleware, after: status 500
7. global middleware, after
10. defer()->always()
8. global middleware terminate()
9. terminating callback (after the response was sent)

The /trace/denied run (a 403) matched the 500 one except for the 5a line. Four things in the result are easy to get wrong:

  • An exception does not skip the middleware on the way back. The router’s pipeline catches it at the innermost layer and turns it into a response with the exception handler, after reporting it (5a), so the route and global middleware still ran their "after" code, and saw a 500. A middleware that adds headers or writes an audit line on the way out therefore does it for errors as well; one that must not, checks the status.

  • abort(403) was not reported. HttpException and AuthorizationException are on the handler’s internal "do not report" list, so a 403 or 404 is a response, not an incident.

  • Deferred functions run only below status 400, so the plain defer() ran for the 200 only, and the one marked ->always() ran for all three. Both are invoked, and their exceptions reported and swallowed, by the terminate() method of the global InvokeDeferredCallbacks middleware.

  • That is also why 10 came before 8. The kernel calls terminate() on the global middleware in stack order, and InvokeDeferredCallbacks is second in that stack while the traced middleware was appended last. The terminating callback registered in the provider runs after all terminable middleware. Code that must run after a deferred function cannot rely on being a later terminate(); it belongs in the same deferred closure.

Outside HTTP, deferred functions run when the Artisan command or queued job that registered them completes successfully, so defer() in a service class behaves the same way wherever that class is used.

Under Octane

With Laravel Octane (FrankenPHP, Swoole or RoadRunner), the application is booted once per worker and kept in memory: steps 1 and 2 above (the bootstrappers, register() and boot()) do not run per request, only the middleware, routing and controller do. Octane resets the framework’s own request-scoped state between requests, but singletons and static properties of the application persist, the same pitfall shown with plain FrankenPHP on The PHP Request Model and Servers. Details are on Octane and Performance.

Where older books and tutorials are outdated

  • The HTTP kernel class with $middleware, $middlewareGroups and $routeMiddleware (Stauffer ch. 10) no longer exists in new applications: the stacks are configured in bootstrap/app.php, and the defaults live in the framework (Laravel 11).

  • VerifyCsrfToken in the web group is PreventRequestForgery in Laravel 13, which also checks Sec-Fetch-Site.

  • RouteServiceProvider loading the route files and setting $namespace (Stauffer ch. 3) is replaced by withRouting(); controllers are referenced by class, not by namespace strings.

  • Doing work after the response required queues or register_shutdown_function(); defer() (Laravel 11) and terminable middleware cover the simple cases.