Request Lifecycle: How Web Frameworks Handle a Request · PHP Frameworks: Laravel, Octane and Symfony
The Laravel Request Lifecycle
Why even the health-check endpoint took 120 ms, and how Laravel's bootstrap, service providers, middleware and terminate phase fit together.
Scenario
Example app: a Laravel SaaS added a SettingsServiceProvider. Its boot() method loads all tenant settings from the database and calls a feature-flag API, so settings are "always available".
What you see in production
- Every request got about 120 ms slower, including
/up(the health check), static-like JSON endpoints and webhooks that never read settings. - The feature-flag provider's rate limit is hit during traffic peaks, because it's called on every request.
- Profiling shows the time is spent before any middleware or controller runs.
The naive code
class SettingsServiceProvider extends ServiceProvider
{
public function boot(): void
{
$settings = Setting::all()->pluck('value', 'key'); // DB query on every request
$flags = Http::get('https://flags.example.com/all')->json(); // network call on every request
$this->app->instance('settings', $settings->merge($flags));
}
}
Reproduce it
The simulation walks the Laravel lifecycle for two requests, once with the eager provider and once with the work moved to a lazily resolved, cached service.
<?php
function lifecycle(string $path, bool $eagerSettings, bool $trace): int {
$ms = 0;
$step = function (string $text, int $cost = 0) use (&$ms, $trace) {
$ms += $cost;
if ($trace) echo " $text" . ($cost ? " (+{$cost} ms)" : "") . "\n";
};
$step("public/index.php: autoloader, create app from bootstrap/app.php", 1);
$step("HTTP kernel bootstrappers: env, config, exception handling, facades", 2);
$step("register() every service provider", 1);
$step("boot() every service provider" . ($eagerSettings ? " - SettingsServiceProvider queries DB + flag API" : ""),
$eagerSettings ? 120 : 2);
$step("-> global middleware (trusted proxies, maintenance mode, trim strings)", 1);
$step("router matches $path, applies route middleware group");
if ($path === "/checkout") {
$step("-> route middleware: auth, verified", 3);
$step("controller resolved from the container; settings service resolved now (cached)", $eagerSettings ? 0 : 4);
$step("controller runs", 25);
} else {
$step("controller runs", 1);
}
$step("<- response travels back out through the same middleware (in reverse)");
$step("response sent to the client");
$step("terminate(): terminable middleware and app terminating callbacks run after sending");
return $ms;
}
echo "trace of GET /up with the eager provider:\n";
lifecycle("/up", true, true);
foreach (["/up", "/checkout"] as $path) {
printf("%-10s eager provider: %3d ms | lazy cached service: %3d ms\n", $path, lifecycle($path, true, false), lifecycle($path, false, false));
}
Output
trace of GET /up with the eager provider: public/index.php: autoloader, create app from bootstrap/app.php (+1 ms) HTTP kernel bootstrappers: env, config, exception handling, facades (+2 ms) register() every service provider (+1 ms) boot() every service provider - SettingsServiceProvider queries DB + flag API (+120 ms) -> global middleware (trusted proxies, maintenance mode, trim strings) (+1 ms) router matches /up, applies route middleware group controller runs (+1 ms) <- response travels back out through the same middleware (in reverse) response sent to the client terminate(): terminable middleware and app terminating callbacks run after sending /up eager provider: 126 ms | lazy cached service: 8 ms /checkout eager provider: 153 ms | lazy cached service: 39 ms
Root cause
In a standard PHP-FPM deployment, Laravel bootstraps the whole application on every request. All service providers run register() and then boot() before routing even starts. Expensive work in boot() is therefore paid by every request, whether or not it needs the result.
The lifecycle, step by step
From Laravel's request lifecycle documentation:
- Entry:
public/index.phploads Composer's autoloader and the application instance frombootstrap/app.php. - Kernel bootstrapping: the HTTP kernel loads environment and configuration, sets up error handling and facades, then registers all service providers and boots them. Providers wire the framework (database, queue, routing, validation) and your app into the service container.
- Global middleware: the request passes through middleware that runs for every request.
- Routing: the router matches a route, applies its middleware group (
web: sessions, cookies, CSRF;api: stateless) and route middleware (auth,throttle), and resolves route-model bindings. - Controller: resolved from the container with dependencies injected; its return value (view, array, model,
Response) is converted to a response. - Response: travels back out through the same middleware in reverse order (the "onion"), so middleware can modify it, then is sent.
- Terminate: terminable middleware (
terminate()method) andapp()->terminating()callbacks run after the response is sent (on PHP-FPM, afterfastcgi_finish_request).
The fix
- Keep
register()to bindings andboot()cheap: no queries or HTTP calls in providers. - Bind expensive services lazily (a singleton closure resolves only when first used) and cache their data (
Cache::remember), refreshing on change. - Use deferred providers (implement
DeferrableProvider) for providers that only bind services, so they load only when one of those services is requested. - In production run
php artisan optimize(config, routes, events and views caching) so bootstrap doesn't parse files on each request. - Use terminable middleware or queued jobs for work that doesn't need to block the response.
Trade-offs
- Caching settings means changes take effect after the cache is invalidated; invalidate on save.
- Long-running servers (Octane) avoid repeated bootstrap entirely but bring their own risks (next lesson).
Detect it in production
- Profile requests (Laravel Telescope, Debugbar locally, APM in production) and look at time before the first middleware.
- Compare the latency of a trivial endpoint (
/up) with the network round trip: if a no-op takes 100 ms, bootstrap is the culprit.
Key points
- Laravel bootstraps the app and boots all providers on every request (under PHP-FPM).
- Order: index.php → kernel bootstrap → providers register/boot → global middleware → router and route middleware → controller → response back through middleware → send → terminate.
- Keep providers cheap; resolve and cache expensive services lazily; use
php artisan optimize.
Practice questions
Walk through the lifecycle of an HTTP request in Laravel.
Answer
public/index.php loads the autoloader and app; the HTTP kernel bootstraps (env, config, error handling, facades) and registers then boots all service providers; the request passes global middleware; the router matches a route and runs its middleware group and route middleware; the controller runs; the response goes back through middleware in reverse and is sent; terminable middleware runs afterwards.
Why should a service provider's boot() method not run database queries or HTTP calls?
Answer
Under PHP-FPM every request boots every provider before routing, so the cost is paid by all requests, even those that never need the data. Resolve such services lazily and cache their data.
What is terminable middleware in Laravel?
Answer
Middleware with a terminate($request, $response) method that runs after the response has been sent to the client, useful for work such as logging that shouldn't delay the response.
Open in the app to mark lessons complete, take notes and track your progress.