The Laravel request lifecycle is the path every HTTP request takes through your application. It starts at public/index.php, which loads Composer and creates the application from bootstrap/app.php. The HTTP kernel then loads the configuration and boots your service providers.

Next, the request passes through global middleware, gets matched to a route, and runs that route's middleware and controller. Finally, the response travels back through the same middleware and is sent to the browser. Terminating tasks run afterwards.

Most developers can build a complete CRUD app without understanding any of this. Then something unexpected happens. Middleware runs twice, a service provider slows down every request, or a value set in one request appears in another under Octane. At that point, understanding how Laravel works stops being an interview question and becomes a debugging skill.

This guide walks through each stage of the Laravel lifecycle in order, using the Laravel 11+ file structure, so it applies directly to Laravel 13. It concludes with the lifecycle questions interviewers commonly ask.

Key Takeaways

  • Every request starts at public/index.php and ends when the response is sent, followed by a termination phase for cleanup work.

  • Service providers handle bootstrapping. All providers run register() first, followed by boot(). Slow code in boot() affects every request.

  • Middleware wraps the request like layers of an onion. Code before $next($request) runs on the way in, while code after it runs on the way out.

  • Since Laravel 11, there is no app/Http/Kernel.php in new applications. Middleware, routing, and exception handling are configured in bootstrap/app.php.

  • Under Octane, the application boots once and serves multiple requests, so state stored in singletons or static properties can leak between users.

The developers in this article's examples are illustrative composites based on common scenarios.

The Laravel Request Lifecycle at a Glance

Here's the complete journey of a request (official lifecycle documentation):

Step

Where It Happens

What It Does

1. Entry point

public/index.php

Checks maintenance mode and loads Composer's autoloader

2. Create the app

bootstrap/app.php

Builds the service container with routing, middleware, and exception configuration

3. HTTP kernel

Illuminate\Foundation\Http\Kernel

Runs bootstrappers for the environment, configuration, error handling, and facades

4. Service providers

bootstrap/providers.php + packages

Runs each provider's register(), followed by boot()

5. Global middleware

bootstrap/app.php

Runs on every request: proxies, CORS, maintenance mode, input trimming

6. Routing

Router

Matches the URL and HTTP method to a route

7. Route middleware

web / api groups, route-specific

Handles sessions, CSRF, authentication, and model binding

8. Controller

Your code

Handles the request and returns a response

9. Response

Back through middleware

Each layer can modify the response

10. Terminate

After sending

Runs terminable middleware and deferred tasks

The rest of this article explores each step in detail.

Step 1: public/index.php, the Entry Point

Your web server (nginx, Apache, or FrankenPHP) points its document root to public/. Every request that isn't a static file is sent to public/index.php. In a Laravel 13 application, the file is short:

<?php

use Illuminate\Foundation\Application;
use Illuminate\Http\Request;

define('LARAVEL_START', microtime(true));

// Load the maintenance-mode file if `php artisan down` is active
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
    require $maintenance;
}

// Composer's autoloader
require __DIR__.'/../vendor/autoload.php';

// Create the application and handle the incoming request
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';

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

Request::capture() converts PHP's superglobals ($_GET, $_POST, $_SERVER, cookies, and files) into a single Illuminate\Http\Request object. From this point on, Laravel works with that object rather than the raw globals.

Notice that the maintenance check happens before the framework loads. By default, Laravel still needs to boot partially to detect maintenance mode, and the PreventRequestsDuringMaintenance middleware returns the 503 page. If you pre-render the page using php artisan down --render="errors::503", it is served from this early check without booting the framework.

Step 2: bootstrap/app.php Creates the Application

bootstrap/app.php builds the application instance, which also acts as Laravel's service container:

// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // add, remove or reorder middleware here
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // customize exception reporting and rendering here
    })
    ->create();

Before Laravel 11, this configuration was spread across app/Http/Kernel.php, RouteServiceProvider, and app/Exceptions/Handler.php. Now, it lives in one file. If you're upgrading an older application or following an older tutorial, this is the biggest structural change to understand.

Step 3: The Laravel Kernel Bootstraps the Framework

handleRequest() passes the request to the HTTP kernel. The kernel is central to how Laravel works: it takes a request in and returns a response out.

Before handling anything, the kernel runs a list of bootstrappers. They:

  • Load environment variables from .env (or skip it when configuration is cached)

  • Load configuration files from config/

  • Set up error and exception handling

  • Register facades

  • Register service providers, then boot them

The second point explains why php artisan config:cache matters in production. Cached configuration is read from a single file, while uncached configuration requires reading and merging every file in config/ on each request.

There's also a console kernel for Artisan commands. php artisan follows a similar boot sequence, but dispatches to a command rather than a route.

Step 4: Service Providers Boot Your App

The service provider documentation describes providers as the central place for bootstrapping a Laravel application. The lifecycle shows why. Your providers are listed in bootstrap/providers.php. Package providers are discovered automatically through their composer.json.

Laravel loads providers in two passes:

  1. register() on every provider. Only bind things into the container here. Don't use other services yet, because they may not be registered.

  2. boot() on every provider. Everything is registered now, so you can use any service to define gates, register view composers, set rate limiters, or add model observers.

// app/Providers/AppServiceProvider.php
public function register(): void
{
    // Bind interfaces to implementations
    $this->app->singleton(PaymentGateway::class, StripeGateway::class);
}

public function boot(): void
{
    // Safe to use other services here
    RateLimiter::for('api', fn (Request $request) =>
        Limit::perMinute(60)->by($request->user()?->id ?: $request->ip())
    );
}

Because this runs on every request, anything slow in boot() adds overhead to your entire application. (Deferred providers are the exception: they load only when one of their services is first used.)

Here's how that can happen. Nadia's team added a boot() method that loaded every row from a settings table and shared it with all views. With 30 settings, the impact was barely noticeable.

Two years later, the table contained 4,000 rows of per-tenant overrides, and every request (including API calls that never rendered a view) spent about 120ms loading them. Moving the query into a cached, lazily loaded service cut the median response time by almost half.

Want the boilerplate handled for you? StackMivo generates complete Laravel apps from a plain-English description, with service providers, middleware, routes, and controllers wired up using current Laravel conventions.

Step 5: Global Middleware

Once the application is booted, the request passes through global middleware, which runs on every request. Laravel's defaults include middleware that:

  • Trusts proxy headers from load balancers (TrustProxies)

  • Handles CORS preflight requests (HandleCors)

  • Blocks requests during maintenance mode (PreventRequestsDuringMaintenance)

  • Rejects oversized POST bodies (ValidatePostSize)

  • Trims whitespace from input and converts empty strings to null

  • Runs defer() callbacks after the response (InvokeDeferredCallbacks)

You can add your own middleware in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\App\Http\Middleware\AddSecurityHeaders::class);
})

How Middleware Wraps the Request

Middleware works like layers of an onion (middleware documentation). Each layer receives the request and a $next closure:

public function handle(Request $request, Closure $next): Response
{
    // 1. Runs BEFORE the controller (on the way in)
    $start = microtime(true);

    $response = $next($request);   // pass to the next layer

    // 2. Runs AFTER the controller (on the way out)
    $response->headers->set('X-Response-Time', round((microtime(true) - $start) * 1000).'ms');

    return $response;
}

A middleware can also stop the request early by returning a response without calling $next(). This is exactly what auth does when it redirects a guest to the login page.

Step 6 and 7: Routing and Route Middleware

The router matches the request's HTTP method and URL to a route. If nothing matches, Laravel returns a 404 (or your fallback route). If the URL matches but the HTTP method doesn't, it returns a 405. Our Laravel routing guide explains how routes, groups, and model binding are defined.

Before your controller runs, the route's middleware executes. Routes in routes/web.php receive the web middleware group, which includes:

  • EncryptCookies: decrypts incoming cookies

  • StartSession: loads the session

  • ShareErrorsFromSession: makes $errors available to views

  • PreventRequestForgery: rejects forms without a valid CSRF token (called ValidateCsrfToken in Laravel 11 and 12, now deprecated)

  • SubstituteBindings: converts {post} into a Post model

API routes receive the lighter api group, without sessions or CSRF. Then, any middleware attached directly to the route runs, such as auth, verified, or throttle:api.

Notice where route model binding happens: inside the SubstituteBindings middleware. That's why a missing model returns a 404 before your controller is called.

Step 8: The Controller Returns a Response

Finally, your code runs. The container builds the controller and injects any dependencies type-hinted in its constructor or method:

public function show(Post $post, PostViewCounter $counter): View
{
    $counter->increment($post);   // resolved from the container

    return view('posts.show', ['post' => $post]);
}

Whatever you return is converted into a response object. A view becomes an HTML response, an array or Eloquent model becomes JSON, and a string becomes an HTML response. If an exception is thrown anywhere in the lifecycle, the exception handler configured in withExceptions() converts it into an error response.

Step 9 and 10: Sending the Response and Terminating

The response now travels back through every middleware layer in reverse order. This is where the "after" code from Step 5 runs: adding headers, writing cookies, and saving the session.

Laravel then sends the response to the browser. However, the lifecycle isn't over. During the terminate phase, Laravel calls terminate() on any middleware that defines it, along with registered terminating callbacks.

On servers that support it (such as PHP-FPM), this happens after the user has already received the response, making it a good place for work the user shouldn't have to wait for. Since Laravel 11.23, the defer() helper makes this easy:

use function Illuminate\Support\defer;

public function store(StoreOrderRequest $request)   // a FormRequest
{
    $order = Order::create($request->validated());

    // Runs after the response is sent
    defer(fn () => Metrics::reportOrderPlaced($order));

    return to_route('orders.show', $order);
}

Deferred callbacks are skipped when the response is a 4xx or 5xx error. Chain ->always() if they must run regardless. Use defer() for quick tasks such as metrics or logging. Anything slow, retryable, or critical belongs on a queue, because deferred work can be lost if the process dies.

The Laravel Request Lifecycle Under Octane

With PHP-FPM, every request runs Steps 1 to 10 from scratch, and PHP then discards everything. Laravel Octane changes this. It boots the application once per worker, keeps it in memory, and reuses it for multiple requests (workers restart after 500 requests by default). That's why Octane is fast: Steps 1 to 4 mostly disappear.

The trade-off is that anything stored in long-lived objects survives between requests.

Leo learned this in production. His application stored the current tenant in a singleton service. Under PHP-FPM, every request started fresh. After moving to Octane, users occasionally saw another customer's dashboard for a few seconds because the singleton still held the previous request's tenant.

Binding the service with scoped() instead of singleton() fixed the issue, since scoped bindings are reset for each request.

Our Laravel Octane performance guide covers memory leaks and safe state patterns in detail.

Ship faster without memorizing every internal. Describe your app, and StackMivo generates it using current Laravel conventions, ready for PHP-FPM or Octane. Try StackMivo free →

Laravel Request Lifecycle Interview Questions

The lifecycle comes up in almost every mid-level Laravel interview. Here are the questions we commonly see, with answers you can give in under a minute. For a broader list, see our Laravel interview questions guide.

1. Walk me through the Laravel request lifecycle.

The request enters public/index.php, which loads Composer and creates the application from bootstrap/app.php. The HTTP kernel bootstraps configuration and service providers, then sends the request through global middleware, the router, and route middleware to the controller. The response returns through middleware, is sent, and termination tasks run.

2. What's the difference between register() and boot()?

register() only binds services into the container and runs for every provider first. boot() runs after all providers have been registered, so it can safely use other services.

3. Where did app/Http/Kernel.php go in Laravel 11?

It was removed from the application skeleton for new apps (upgraded apps can keep theirs). Middleware is now configured in bootstrap/app.php using withMiddleware(). The framework's kernel class still exists inside Laravel itself.

4. How does middleware run code after the controller?

Code placed after $response = $next($request) runs on the way out, once the controller has returned a response.

5. What changes when you run Laravel on Octane?

The application boots once and stays in memory. Providers don't run again for every request, so singletons and static state can leak between requests. Use scoped bindings and avoid storing request data in long-lived objects.

Conclusion

The Laravel request lifecycle follows a predictable path: public/index.php, application creation, kernel bootstrapping, service providers, global middleware, routing, route middleware, your controller, and finally the response and termination phase. Understanding this order explains most unexpected Laravel behavior, from slow requests to missing sessions and Octane state leaks.

The next time something behaves unexpectedly, ask which stage of the lifecycle it occurs in. If you're planning what to learn next, our Laravel developer roadmap puts these internals in context with the rest of the stack.

Build your next Laravel app with StackMivo, free to start →