What is the Request Lifecycle?
Every request your browser sends goes through a fixed, predictable journey inside Laravel.
It's not magic — it's a well-defined sequence of steps that always happen in the same order.
Understanding this journey is one of the highest-leverage things you can do as a Laravel developer.
Once you know how a request travels from the browser to your controller and back, nothing in the
framework feels mysterious anymore. You know exactly where to look when something breaks, and exactly
where to hook in when you need to add behavior.
Here's the full flow at a glance:
Browser / Client
│ HTTP Request
▼
public/index.php ← Single entry point
▼
Service Container ← App's "brain" (DI/IoC)
▼
HTTP Kernel ← Runs bootstrappers
▼
Service Providers ← Wires up the entire framework
▼
Middleware Stack ← Request passes through layers (incoming)
▼
Router ← Matches URL → Controller
▼
Controller / Logic ← Your code runs here
▼
Middleware Stack ← Response passes back through layers (outgoing)
▼
send() ← Response delivered to browser
Let's walk through each step in detail.
Entry Point: public/index.php
When a request hits your server, Nginx or Apache is configured to send every URL
to a single file: public/index.php. This is the only public PHP file in a Laravel application.
Everything else — your models, controllers, config — lives outside the public/ directory
and is never directly accessible via the browser.
public/index.php does exactly two things:
Loads the Composer autoloader — require vendor/autoload.php — which makes every package and class in your app available without manual require calls.
Boots the application — require bootstrap/app.php — which creates the Application instance (the Service Container) and hands it to the HTTP Kernel.
<?php
// public/index.php (simplified)
// 1. Load Composer autoloader
require __DIR__.'/../vendor/autoload.php';
// 2. Boot the application
$app = require_once __DIR__.'/../bootstrap/app.php';
// 3. Handle the request
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
⚠️ Pro tip: Never put application logic in public/index.php. It is a bootstrap door — nothing more. All customization belongs in Service Providers or Middleware.
The Service Container
The moment bootstrap/app.php runs, it creates the core Application object.
This object is the Service Container — also called the IoC (Inversion of Control)
container or DI (Dependency Injection) container.
Think of the Service Container as a smart registry. You tell it: "when someone asks for interface X,
give them an instance of class Y." From that point on, Laravel can automatically build any class in
your app, injecting all its dependencies automatically.
The container is the foundation everything else is built on:
Every facade resolves through the container
Every controller dependency is injected from the container
Every service provider binds its services into the container
⚠️ Key fact: Laravel rebuilds the entire Service Container on every request. Services are not shared between requests (unless you're using Laravel Octane or a long-running server like Swoole).
HTTP Kernel & Bootstrappers
After the container is built, the HTTP Kernel takes over. The Kernel has one job:
receive an HTTP Request and return an HTTP Response. Everything that happens in between is managed by the Kernel.
Before it touches the request, the Kernel runs a series of bootstrappers — classes
that set up the framework itself:
Bootstrapper Purpose
────────────────────────────── ───────────────────────────────────────
LoadEnvironmentVariables Reads your .env file
LoadConfiguration Loads all files in config/
HandleExceptions Registers error/exception handlers
RegisterFacades Makes Facade aliases work (e.g. Cache::, DB::)
RegisterProviders Registers all Service Providers (register phase)
BootProviders Calls boot() on all Service Providers
The bootstrappers run once per request, in order. By the time they finish, your entire application
is wired up and ready to handle the request.
The Kernel is a "black box" from the outside:
Everything in between — providers, middleware, routing, your controller — is managed inside that black box.
Service Providers – The Most Important Step
Service Providers are the heart of the Laravel bootstrapping process. Every major feature of the
framework is wired up through a Service Provider:
Database and Eloquent ORM
Queue system
Validation
Routing
Mail, Cache, Events, Auth, Broadcasting...
Your own providers live in app/Providers/ and are listed in bootstrap/providers.php.
Two-Phase Loading
Service Providers load in two distinct phases. This is important to understand:
Phase 1 — register()
─────────────────────────────────────────────────────
All providers call register() first — before any boot() is called.
Only bind things into the container here.
Do NOT depend on other services (they may not be registered yet).
Phase 2 — boot()
─────────────────────────────────────────────────────
Called after ALL providers have completed register().
It is safe to use any service here.
Use boot() for: event listeners, view composers, route macros,
model observers, validation rules, etc.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Phase 1: bind into the container
$this->app->singleton(PaymentGateway::class, function ($app) {
return new StripeGateway(config('services.stripe.key'));
});
}
public function boot(): void
{
// Phase 2: safe to use other services
\Illuminate\Support\Facades\View::composer('*', function ($view) {
// share data to all views
});
}
}
⚠️ Common mistake: calling another service inside register(). If that service hasn't been registered yet, you'll get a "not found in container" error. Move cross-service logic to boot().
Middleware Stack (Incoming)
Once the Kernel is bootstrapped, the incoming request passes through the Middleware Stack
before it reaches your controller. Middleware are layers that wrap around your application logic —
each one gets a chance to inspect or modify the request, or short-circuit it entirely.
Two Types of Middleware
Global middleware — runs on every single request, regardless of route
Route middleware — runs only for specific routes or route groups
Common Built-in Middleware
Middleware What it does
─────────────────────────────────── ─────────────────────────────────────────
PreventRequestsDuringMaintenance Returns 503 if app is in maintenance mode
ValidateCsrfToken Verifies CSRF token on POST/PUT/DELETE
StartSession Reads/writes the user session
Authenticate Checks if user is logged in
TrimStrings Trims whitespace from all input strings
ConvertEmptyStringsToNull Converts "" to null on all inputs
Middleware runs in the order it's registered. The request enters the first middleware,
passes through each one in sequence, and arrives at the controller only after all middleware
have approved it.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class LogRequestMiddleware
{
public function handle(Request $request, Closure $next)
{
// Before: inspect/modify the incoming request
logger('Incoming: ' . $request->method() . ' ' . $request->path());
$response = $next($request); // Pass to the next layer
// After: inspect/modify the outgoing response
logger('Outgoing: ' . $response->getStatusCode());
return $response;
}
}
The Router
After passing through global middleware, the request reaches the Router.
The Router matches the incoming URL and HTTP method against your route definitions
in routes/web.php or routes/api.php.
When a match is found, the Router:
Runs any route-specific middleware assigned to that route or group
Resolves route parameters (e.g. {id} in the URL)
Performs route model binding (automatically fetches the model if you type-hint it)
Dispatches to the Controller method or Closure defined for that route
// routes/web.php
// Closure route
Route::get('/hello', function () {
return 'Hello World';
});
// Controller route
Route::get('/users/{user}', [UserController::class, 'show']);
// Route with middleware
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('auth');
// Route group
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/profile', [ProfileController::class, 'edit']);
Route::put('/profile', [ProfileController::class, 'update']);
});
If no route matches, Laravel returns a 404 response. You can customize this
in your exception handler.
Controller / Response
This is where your code finally runs. The controller method receives the
Request object (with all input, files, and headers) and whatever dependencies
Laravel injected from the container.
The controller is responsible for one thing: returning a Response.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function show(User $user) // ← route model binding auto-fetches user
{
// Your logic runs here
return view('users.show', ['user' => $user]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
$user = User::create($validated);
return response()->json($user, 201); // JSON response
}
}
A controller can return many response types:
View — return view('welcome');
JSON — return response()->json($data);
Redirect — return redirect()->route('home');
File download — return response()->download($path);
Plain string — return 'Hello'; (auto-converted to a response)
Middleware Stack (Outgoing) & send()
Once your controller returns a response, it travels back through the middleware stack
in reverse order. Each middleware gets a second chance — this time to inspect or
modify the outgoing response.
Common things done on the outgoing pass:
Adding response headers (CORS headers, cache-control, etc.)
Encrypting cookies
Writing the session to storage
Logging the response status
After all middleware have processed the response, the Kernel calls send(),
which writes the HTTP response (headers + body) to the browser.
Finally, the Kernel calls terminate() — this runs any post-response cleanup
(for example, writing log buffers, terminating database connections) after the
response has already been sent to the user.
Key Files Cheatsheet
Here's a quick reference for the files involved in the request lifecycle:
File / Directory Role
───────────────────────────────── ────────────────────────────────────────────
public/index.php Single entry point for all web requests
bootstrap/app.php Creates the Application + Service Container
bootstrap/providers.php Lists all registered Service Providers
app/Providers/AppServiceProvider Your app's main service provider
app/Providers/ All your custom service providers
routes/web.php Web routes (with session, CSRF middleware)
routes/api.php API routes (stateless, no session by default)
app/Http/Middleware/ Your custom middleware classes
config/app.php App-wide configuration (timezone, locale, etc.)
.env Environment variables (read by LoadEnvironmentVariables)
Mental Model – The Onion
The best way to think about the request lifecycle is as an onion.
The request peels through each layer on the way in, your code runs at the core,
and the response passes back through the same layers on the way out.
┌──────────────────────────────────────────┐
│ Global Middleware │
│ ┌──────────────────────────────────┐ │
│ │ Route Middleware │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ Controller / Logic │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
Request ──────────────────────────────────►
Response ◄──────────────────────────────────
Middleware wraps your app. The request goes in, gets processed at the core, and the response
comes back out through the same wrapping. This is the decorator pattern applied to HTTP handling.
Pro Developer Insights
1. Never touch public/index.php
All customization starts at Service Providers or Middleware. The entry point is a bootstrap
door — it shouldn't contain any application logic.
2. Service Providers are your app's wiring
Any time you add a package or major feature, it registers itself via a Service Provider.
This is also how you bind your own interfaces to implementations, giving you full control
over what gets injected where.
3. register() vs boot() — don't mix them up
register() → bind things into the container (no dependencies on other services)
boot() → use services, set up listeners, macros, observers — everything else
4. Middleware is composable
You can stack, order, and group middleware. They are the right place for cross-cutting
concerns — authentication, logging, rate limiting, CORS, input sanitization. Keep controllers
lean by pushing these concerns into middleware.
5. The Kernel is not your enemy
You rarely need to modify the Kernel directly. Laravel's default setup handles everything.
Understand it conceptually, but don't feel you need to touch it.
6. Every request is stateless at the PHP level
Laravel rebuilds the entire container on every single request. There is no shared state
between requests unless you explicitly use an external store (database, cache, Redis).
This is why you can scale Laravel horizontally without worrying about in-process state.
Conclusion
The Laravel request lifecycle is a clean, predictable pipeline. Once you internalize it,
you gain the confidence to debug any issue, hook into the right place, and reason about
your application at a deeper level.
All requests enter through public/index.php — the only public PHP file
The Service Container is built immediately and powers everything else
The HTTP Kernel runs bootstrappers: loads env, config, and all Service Providers
Service Providers wire up the framework in two phases: register() then boot()
The request passes through the Middleware stack (incoming), then the Router dispatches to your Controller
The response travels back through Middleware (outgoing) before being sent to the browser