Copied!
Laravel
PHP

Laravel Request Lifecycle Explained – How Every Request Is Handled

Laravel Request Lifecycle Explained – How Every Request Is Handled
Shahroz Javed
Apr 08, 2026 . 108 views

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:

  1. Loads the Composer autoloaderrequire vendor/autoload.php — which makes every package and class in your app available without manual require calls.

  2. Boots the applicationrequire 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:

  • Input → HTTP Request object

  • Output → HTTP Response object

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:

  1. Runs any route-specific middleware assigned to that route or group

  2. Resolves route parameters (e.g. {id} in the URL)

  3. Performs route model binding (automatically fetches the model if you type-hint it)

  4. 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:

  • Viewreturn view('welcome');

  • JSONreturn response()->json($data);

  • Redirectreturn redirect()->route('home');

  • File downloadreturn response()->download($path);

  • Plain stringreturn '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

📑 On This Page