Laravel Request Lifecycle
When a user types a URL and hits Enter, something happens before your controller method even runs. Laravel does not magically jump from a browser request to your code. There is a precise sequence of steps that every single request travels through — loading the framework, booting services, running middleware, matching a route, executing a controller, and finally returning a response.
Most beginners treat this as a black box. That works until something goes wrong — a middleware not running when expected, a service not available when you need it, an error you cannot trace to a file. Understanding the request lifecycle removes that confusion permanently.
This lesson walks through each step of that journey in detail, with real code showing what actually happens at each stage.
The Full Lifecycle at a Glance
Browser Request
↓
1. public/index.php — entry point, loads the framework
↓
2. bootstrap/app.php — creates the application instance
↓
3. HTTP Kernel — coordinates the entire request
↓
4. Service Providers — boot all application services
↓
5. Middleware (global) — filters applied to every request
↓
6. Router — matches URL to a route definition
↓
7. Route Middleware — filters applied to specific routes
↓
8. Controller / Closure — your application logic runs
↓
9. Response built — view rendered or JSON prepared
↓
10. Middleware (terminate) — cleanup runs after response is sent
↓
Browser Response
Each of these steps has a purpose. Let's go through them one by one.
Step 1 — public/index.php (The Entry Point)
Every single request to a Laravel application — regardless of the URL — enters through one file: public/index.php. Your web server (Nginx or Apache) is configured to direct all traffic here. This is why it is called the "front controller" pattern.
// public/index.php (simplified)
// 1. Load Composer's autoloader — makes all PHP classes available
require __DIR__.'/../vendor/autoload.php';
// 2. Create the application instance
$app = require_once __DIR__.'/../bootstrap/app.php';
// 3. Get the HTTP Kernel from the service container
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// 4. Turn the incoming PHP request into a Laravel Request object and handle it
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
// 5. Send the response to the browser
$response->send();
// 6. Run any terminating middleware after the response is sent
$kernel->terminate($request, $response);
These six lines are the entire lifecycle in condensed form. Everything else — service providers, middleware, routing, controllers — happens inside $kernel->handle($request).
Notice that the public/index.php file has no application-specific code. It just bootstraps the framework and delegates. You never edit this file.
Step 2 — bootstrap/app.php (Creating the Application)
The bootstrap/app.php file creates the Laravel application instance — a single object that acts as the central container for everything in your application. This is Laravel's Service Container.
// bootstrap/app.php (simplified)
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
// Bind the HTTP Kernel — handles web requests
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
// Bind the Console Kernel — handles Artisan commands
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
// Bind the Exception Handler — handles errors
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
return $app;
The Service Container is like a smart registry. When something in your application needs a dependency — a database connection, a mailer, a cache driver — the container knows how to create it and provides it automatically. This is called dependency injection, and it is one of Laravel's most powerful features.
Step 3 — The HTTP Kernel (The Request Coordinator)
The HTTP Kernel (app/Http/Kernel.php) is the main coordinator for every web request. It has two important jobs:
1. It defines the global middleware stack — middleware that runs on every request without exception:
// app/Http/Kernel.php
protected $middleware = [
// Prevents the app from running when in maintenance mode
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
// Validates POST data size limits
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
// Trims whitespace from all string inputs automatically
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
// Converts empty strings to null
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
2. It defines middleware groups — sets of middleware that apply to specific route groups:
protected $middlewareGroups = [
// Applied to all routes in routes/web.php
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, // CSRF protection
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
// Applied to all routes in routes/api.php
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
This is why your web routes automatically have session support and CSRF protection — the web middleware group handles it. And why API routes have rate limiting but no sessions — the api group is configured that way.
Step 4 — Service Providers (Booting the Application)
Before any request logic runs, Laravel boots all registered Service Providers. This is the most important step most developers overlook.
Service Providers are classes responsible for setting up parts of the application — registering database connections, binding classes into the service container, setting up event listeners, and preparing every tool your app will need.
// config/app.php — the list of providers that boot on every request
'providers' => [
// Core Laravel providers (always run)
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
// ... many more
// Your application's own providers
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
];
Each provider has a register() method and a boot() method:
// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
// register() — bind things into the service container
public function register(): void
{
// Tell the container: whenever someone needs PostService, create it like this
$this->app->bind(PostService::class, function ($app) {
return new PostService($app->make(PostRepository::class));
});
}
// boot() — runs after all providers are registered
// Use this for things that depend on other services already being registered
public function boot(): void
{
// Force HTTPS in production
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
}
}
The distinction matters: register() only binds things into the container. boot() can use those bindings because all providers have already been registered by that point.
Step 5 — Global Middleware Runs
After the application is booted, the global middleware stack runs. Think of middleware as a series of checkpoints every request must pass through before reaching your code.
Middleware wraps around the request in layers — like an onion. The request passes inward through each layer, your controller runs, and then the response passes back outward through the same layers in reverse.
// How middleware works internally
class AuthenticateMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// Check BEFORE the request reaches the controller
if (!auth()->check()) {
return redirect('/login');
}
// Pass to the next middleware or controller
$response = $next($request);
// Optionally do something AFTER the response is built
// (add headers, log the response, etc.)
return $response;
}
}
The $next($request) call is what passes the request to the next layer. If a middleware returns early (like the redirect to login above), the request never reaches the controller at all.
Step 6 — The Router Matches the URL
Once global middleware has run, the Router takes over. It looks at the incoming URL and HTTP method (GET, POST, PUT, DELETE) and finds the matching route definition in your routes/ files.
// routes/web.php
Route::get('/profile/{user}', [ProfileController::class, 'show'])
->middleware('auth')
->name('profile.show');
If no route matches the URL, Laravel automatically returns a 404 response. If the URL matches but the HTTP method does not (a POST request to a GET-only route), Laravel returns a 405 Method Not Allowed response.
Laravel also handles route model binding at this stage. If your route has a {user} parameter and your controller type-hints a User model, Laravel automatically fetches the user from the database:
// Laravel resolves {user} to a User model automatically
public function show(User $user)
{
return view('profile.show', compact('user'));
// No need to write User::findOrFail($id) manually
}
Step 7 — Route Middleware Runs
After the route is matched, any middleware attached specifically to that route runs. This is separate from global middleware.
// Middleware on a single route
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware(['auth', 'verified', 'subscription']);
// Middleware on a group of routes
Route::middleware(['auth', 'role:admin'])->prefix('admin')->group(function () {
Route::get('/users', [AdminUserController::class, 'index']);
Route::get('/orders', [AdminOrderController::class, 'index']);
});
Route middleware lets you apply filters selectively. The auth middleware only needs to run on protected routes — not on the homepage or login page. This is more efficient and more precise than running every check on every request.
Step 8 — Controller Executes
Once all middleware has passed, your controller method finally runs. By this point, the request has been fully validated, authenticated, and prepared. The controller's job — as covered in the MVC lesson — is simply to coordinate: fetch data from the model and pass it to a view.
public function show(User $user)
{
$posts = $user->posts()->published()->latest()->take(5)->get();
$followerCount = $user->followers()->count();
return view('profile.show', compact('user', 'posts', 'followerCount'));
}
Step 9 — Response Is Built and Sent
The controller returns a response — a rendered Blade view, a JSON object, a redirect, or a file download. Laravel wraps this in an HTTP response object with the appropriate headers and status code, then sends it back through the middleware layers (outward through the onion) before it reaches the browser.
// Different types of responses
return view('profile.show', compact('user')); // HTML page
return response()->json(['user' => $user]); // JSON for APIs
return redirect()->route('dashboard'); // Redirect
return response()->download(storage_path('file.pdf')); // File download
Step 10 — Terminate Middleware
After the response is sent to the browser, Laravel runs one final phase: terminating middleware. This is middleware that implements a terminate() method — code that runs after the response has already been delivered.
class LogRequestMiddleware
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
// Runs AFTER the response is sent to the browser
// The user has already seen their page by the time this runs
public function terminate(Request $request, Response $response): void
{
Log::info('Request completed', [
'url' => $request->url(),
'method' => $request->method(),
'status' => $response->getStatusCode(),
'time' => microtime(true) - LARAVEL_START,
]);
}
}
Terminate middleware is useful for logging, analytics, and cleanup tasks that should not delay the user's response. The user gets their page immediately, and the logging happens after.
Why This Understanding Matters in Practice
Knowing the lifecycle solves real debugging problems:
- "My middleware isn't running" — Check whether it is registered in the Kernel's global stack, middleware groups, or only on specific routes. The lifecycle tells you exactly where to look.
- "My service isn't available when I need it" — If you are using a service in a provider's
register()method before it has been registered, it will fail. The lifecycle explains whyboot()exists. - "My route isn't matching" — The router runs after middleware. If a middleware redirects the request, the router never runs. The lifecycle tells you the order to check.
- "Where should I put this logic?" — Need something to run on every request? Middleware. Need something to run once when the app starts? Service provider. Need something per route? Controller. The lifecycle makes this obvious.