The Laravel Request Lifecycle, Step by Step

The Laravel Request Lifecycle, Step by Step

Follow a Laravel HTTP request from start to finish, exploring when the container is built, service providers run, and controllers execute—demystifying the framework step by step.

Steve McDougall

Steve McDougall

April 29, 2026

In the last article, we looked at how the service container resolves dependencies using PHP's reflection API. But there is a question that article leaves hanging: when does all of that actually happen? When does the container get built? When do service providers run? When does your controller finally get called?

That is what this article answers. We are going to trace a single HTTP request from the moment your web server receives it, all the way through to the moment bytes hit the browser. At every step, we will look at what Laravel is actually doing and why it is structured that way.

By the end, the framework will feel a lot less like a black box.


The Entry Point: public/index.php

Every HTTP request to a Laravel application, without exception, lands in public/index.php. Your Nginx or Apache configuration points all traffic here. The file itself is deliberately minimal:

      <?php

use Illuminate\Http\Request;

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

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

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

    

Four things happen here, in order. First, the Composer autoloader is registered. This is what makes every class in your application and every package in vendor/ available without manual require calls. The autoloader registers a PSR-4 mapping and a classmap, and from this point forward, any class reference triggers an automatic file load on first use.

Second, bootstrap/app.php runs. This file constructs the application instance, which is the container you read about in the previous article. We will come back to what that construction actually does in a moment.

Third, Request::capture() builds an Illuminate\Http\Request object from the current PHP superglobals: $_GET, $_POST, $_FILES, $_COOKIE, $_SERVER. This wraps the raw PHP request data into a clean object with a rich API.

Fourth, $app->handleRequest() hands that request to the HTTP kernel and lets the framework take over.

Notice what is not here: no configuration loading, no database connections, no service providers. All of that happens inside handleRequest. The entry point does as little as possible by design. It is a thin bootstrap shim and nothing more.


bootstrap/app.php: Building the Container

Before handleRequest can do anything useful, the application needs to exist. The bootstrap/app.php file is responsible for creating it:

      <?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

    

Application::configure() returns a fluent builder that lets you declare your routes, middleware groups, and exception handling in one readable chain. When create() is called at the end, it produces an Illuminate\Foundation\Application instance.

That Application class extends Container. So the container and the application are literally the same object. When you call app() anywhere in your codebase, you are getting the same instance that was created right here.

During construction, the application registers several base bindings into itself. This includes binding the container to its own instance, binding the PackageManifest (which handles package auto-discovery from vendor/composer/installed.json), and registering three foundational service providers immediately: EventServiceProvider, LogServiceProvider, and RoutingServiceProvider. These three are special because so much of what follows depends on them, and they register only the absolute minimum needed to get the event system, logging, and routing infrastructure online.

The application also sets its base path and derives all other framework paths (config, database, resources, routes, storage) from it. This is how config_path(), resource_path(), and similar helpers know where to look.


The HTTP Kernel: The Request's Control Centre

With the application built, handleRequest resolves the HTTP kernel from the container and calls its handle method:

      // Simplified from Illuminate\Foundation\Application

public function handleRequest(Request $request): void
{
    $kernel = $this->make(HttpKernel::class);

    $response = $kernel->handle($request)->send();

    $kernel->terminate($request, $response);
}

    

The HttpKernel class (Illuminate\Foundation\Http\Kernel) is the true centre of the request lifecycle. Its handle method is beautifully simple in its signature: it takes a Request and returns a Response. Everything Laravel does to process an HTTP request is encapsulated inside that single call.

Before handle can process the request, the kernel runs its bootstrappers.


The Bootstrapping Phase: Before Any Request Logic Runs

The kernel has a private $bootstrappers array that defines a sequence of classes to run before request handling begins. In Laravel 13, the default sequence is:

      protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    \Illuminate\Foundation\Bootstrap\BootProviders::class,
];

    

This sequence is not arbitrary. Each step depends on the ones before it, and the order is strict.

LoadEnvironmentVariables reads your .env file and populates $_ENV and $_SERVER via the Dotenv library. This must run first because everything else, configuration files, service providers, your application code, may need environment values.

LoadConfiguration reads every file in the config/ directory and loads them into the config repository. Crucially, this happens after environment variables are loaded, because config files use env() calls throughout.

HandleExceptions registers PHP error and exception handlers. From this point forward, any uncaught exception or error is caught by Laravel and converted to an error response rather than a raw PHP fatal error page.

RegisterFacades reads the aliases defined in your application and registers them with the PHP autoloader. This is what makes Cache::get(), DB::table(), and similar facade calls available globally. The autoloader learns that when something asks for the class Cache, it should look at Illuminate\Support\Facades\Cache. We will explore how facades actually work in a future article.

RegisterProviders is where your service providers come in for the first time. This bootstrapper reads bootstrap/providers.php, plus any providers discovered via package auto-discovery, and calls register() on all of them. All of them. Every provider's register() method runs before any provider's boot() method starts.

BootProviders then calls boot() on every registered provider, in registration order. By this point, all container bindings from all providers are in place, so boot() methods can safely depend on any binding that any provider registered.

This two-phase provider lifecycle, all register() first, then all boot(), is the solution to a fundamental ordering problem. If boot() ran immediately after each provider's register(), a provider might try to use a service from another provider that had not been registered yet. The two-phase approach guarantees that by the time any boot() runs, the entire container is fully populated.


Service Providers: The Real Bootstrap Work

<function_calls> The bootstrapping phase is almost entirely service providers doing their job. It is worth understanding exactly what happens during register() versus boot() because getting this wrong is one of the most common sources of confusing bugs.

register() must only bind things into the container. Nothing else. The rule exists because when your provider's register() runs, you have no guarantee that any other provider has run yet. If you try to resolve a service, listen to an event, or access a facade inside register(), you may be working with an incompletely bootstrapped application:

      public function register(): void
{
    // CORRECT: pure container bindings only
    $this->app->singleton(ReportService::class, function ($app) {
        return new ReportService(
            $app->make(ReportRepository::class),
            config('reports.format')
        );
    });
}

    

boot() is where everything else belongs: view composers, event listeners, route model bindings, gate definitions, blade directives, observer registrations, and anything that needs to call another service:

      public function boot(): void
{
    // Safe: all providers have registered by now
    Gate::define('manage-reports', fn (User $user) => $user->hasRole('analyst'));

    Report::observe(ReportObserver::class);

    View::composer('reports.*', ReportComposer::class);
}

    

Deferred providers, providers that implement DeferrableProvider, are a special case. Their provides() method declares which bindings they will register, and the framework skips loading them entirely until one of those bindings is actually resolved from the container. For services that are expensive to set up and not needed on every request, this is a meaningful performance optimisation.


Routing: Finding Where the Request Goes

Once bootstrapping is complete, the kernel passes the request into the router:

      // Inside Kernel::handle(), simplified
protected function sendRequestThroughRouter(Request $request): Response
{
    return (new Pipeline($this->app))
        ->send($request)
        ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
        ->then($this->dispatchToRouter());
}

    

This is the pipeline pattern in action. The request passes through your global middleware stack first. These are the middleware that run on every request, things like PreventRequestsDuringMaintenance, TrimStrings, and ConvertEmptyStringsToNull.

Each middleware in the stack receives the request, can inspect or modify it, and then calls $next($request) to pass it along. The request travels inward through the stack like passing through a series of gates. The response then travels back outward through the same gates in reverse order.

Once through the global middleware, the router takes over. It matches the incoming URL and HTTP method against your registered routes. Route matching checks static routes first (direct string comparisons), then moves to parameterised routes, using a compiled regex. The RouteCollection caches compiled route regexes at boot time, so matching itself is fast even with hundreds of routes.

When a matching route is found, the router builds another pipeline, this time for route-level middleware. Auth checks, throttling, CSRF verification, and any custom middleware assigned to that specific route or route group run here. Only after a request passes all route middleware does the route action execute.

The route action, whether a closure, a controller method, or an invokable class, is resolved through the container. This is the moment from the previous article where your controller's constructor gets type-hinted dependencies resolved and injected automatically.


Controller Execution and the Response Journey Back

The controller method runs and returns something. That something gets converted to an Illuminate\Http\Response if it is not one already. A returned array becomes a JSON response. A returned string becomes an HTML response. A returned View gets rendered to HTML. A returned Model or Collection gets serialized to JSON. This coercion happens inside the router's prepareResponse() method.

With the response assembled, the return journey begins. The response travels back outward through the route middleware stack. Each middleware that saw the inbound request now sees the outbound response, and can modify it. A middleware might add a response header, compress the body, or log the request-response pair.

After route middleware, the response continues back through the global middleware stack in reverse order.

When the response has cleared all middleware, the kernel's handle method returns it. Back in handleRequest, the application calls $response->send(), which writes the HTTP status line, headers, and body to the output buffer. PHP's FastCGI handler or SAPI pushes those bytes to the web server, which delivers them to the browser.


terminate(): After the Response is Sent

There is one more step that most developers have never thought about. After the response is sent, $kernel->terminate() runs:

      public function terminate(Request $request, Response $response): void
{
    $this->terminateMiddleware($request, $response);

    $this->app->terminate();
}

    

Terminable middleware, middleware that implements a terminate(Request $request, Response $response) method, runs here. The session middleware, for example, writes the session data to storage during termination rather than before the response is sent. This means the browser receives the response faster, and the session write happens in the brief window after delivery.

The $this->app->terminate() call fires any terminating callbacks registered on the application, which is a hook for packages and application code to run cleanup logic after the response cycle completes.

This is only relevant for traditional synchronous PHP-FPM deployments. Under Octane, where the worker process stays alive across requests, the terminate cycle is followed by a flush that resets scoped bindings and clears per-request state, making the process ready for the next connection.


The Full Picture

Tracing the complete path:

  1. Web server receives HTTP request, routes all traffic to public/index.php
  2. Composer autoloader registers
  3. bootstrap/app.php builds the Application (container) instance
  4. Request::capture() wraps PHP superglobals into a Request object
  5. handleRequest() resolves the HTTP kernel from the container
  6. Kernel runs its bootstrapper sequence: environment, config, exceptions, facades, provider registration, provider boot
  7. All service provider register() methods run (building the container)
  8. All service provider boot() methods run (configuring the application)
  9. The request enters the global middleware pipeline
  10. The router matches the request to a route
  11. The request enters the route middleware pipeline
  12. The route action executes, dependencies injected by the container
  13. The response travels back out through route middleware
  14. The response travels back out through global middleware
  15. $response->send() writes bytes to the browser
  16. terminate() runs terminable middleware and cleanup callbacks

That is the complete journey. Every request, every time.


Why This Matters for Your Certification

The lifecycle is tested across all certification tiers because it underpins everything else in the framework. Junior-level questions test whether you know that public/index.php is the entry point and that service providers run before routing. Mid-level questions dig into the register() versus boot() distinction and what each is allowed to do. Senior-level questions ask about the bootstrapper sequence, why it is ordered the way it is, and what the practical consequences of that ordering are.

At the Artisan Master level, you are expected to reason about the lifecycle in context: why does a singleton carry state across requests under Octane, how does deferred provider loading affect bootstrap time, what does terminable middleware enable that you cannot do before send() is called.

The lifecycle is also the lens through which every other framework feature makes sense. Service providers register things because the container is built first. Facades work because RegisterFacades runs before routing. Route middleware can be auth-aware because BootProviders has already wired up the auth system. Nothing in the framework is arbitrary. It is all consequences of the sequence you just read.

Read this alongside the service container article and the two fit together like a key in a lock.

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)