The request lifecycle
Every page a Drupal site serves goes through the same pipeline: one front controller, a stack of middleware, a router, a controller, and a series of kernel events. This page walks the whole path so that, for any piece of code you write later, you can answer the question “where in the request does this run?”.
What and why
Section titled “What and why”Drupal 11 is built on the Symfony HttpKernel. The contract is small: a kernel receives a Request object and must return a Response object. Everything Drupal does on a page load, including bootstrapping itself, caching, routing, access checking, and rendering, happens between those two objects.
Without this architecture, every entry point would need its own bootstrap logic, and there would be no defined place to intercept a request before the expensive parts run. Because the pipeline is explicit, Drupal can do things like serve a cached page to an anonymous visitor without loading a single module, or convert any uncaught exception into a proper 404 or 403 page.
The entire front controller is web/index.php. It creates a kernel, hands it the request, sends the response, and triggers cleanup:
// Fragment: the heart of web/index.php.$kernel = new DrupalKernel('prod', $autoloader);$response = $kernel->handle($request);$response->send();$kernel->terminate($request, $response);There is no other magic in the entry point. If you can follow what handle() does, you understand how Drupal serves pages.
Mental model
Section titled “Mental model”Think of the pipeline as an onion. The request passes inward through layers; the response passes back out through the same layers in reverse. There are three distinct zones:
- The kernel boundary.
\Drupal\Core\DrupalKernelboots the environment, loadssettings.php, and initializes the service container (compiling it on the first request, loading it from cache afterwards). At this point no modules have run yet. - The middleware stack. The
http_kernelservice is a\Drupal\Core\StackMiddleware\StackedHttpKernel: a chain of services taggedhttp_middleware(a tag marks a service for inclusion in a subsystem), ordered by priority (highest first, outermost layer). Each middleware receives the request, may answer it directly, or passes it inward. - The event-driven core. The innermost layer,
http_kernel.basic, is Symfony’sHttpKernel. It does no work itself; it dispatches kernel events, and event subscribers (services that listen for events) do the routing, access checking, and rendering.
A request enters the front controller and passes inward through the middleware stack, ordered by priority. The page cache middleware sits at priority 200, outside module loading: on a hit for an anonymous request it returns the stored response and the request turns around there, so no modules load. On a miss the request continues inward past the kernel pre-handle middleware at priority 100, where all modules load, and reaches the event-driven kernel. The response then travels back out through the same layers.
The middleware stack
Section titled “The middleware stack”Middleware classes implement Symfony’s HttpKernelInterface and wrap each other. These are the layers core registers, from outermost in:
| Priority | Service | Purpose |
|---|---|---|
| 500 | http_middleware.ajax_page_state | Expands the compressed ajax_page_state query parameter |
| 400 | http_middleware.negotiation | Sets the request format from the _format query parameter |
| 300 | http_middleware.reverse_proxy | Applies reverse proxy settings from settings.php |
| 250 | http_middleware.cors | Handles cross-origin requests when CORS is enabled |
| 200 | http_middleware.page_cache | Serves the anonymous page cache (Page Cache module) |
| 150 | http_middleware.big_pipe | Content-Length handling for streamed BigPipe responses |
| 140 | http_middleware.content_length | Adds the Content-Length header to responses |
| 100 | http_middleware.kernel_pre_handle | Calls back into the kernel to finish bootstrapping |
| 50 | http_middleware.session | Starts the session and saves it on the way out |
Two layers deserve special attention:
- The page cache runs at priority 200, far outside module loading. When the Page Cache module has a hit for an anonymous request, it returns the stored response right there. Your hooks, your event subscribers, and your controllers never run. This is the single most important fact for reasoning about “why didn’t my code execute?”.
http_middleware.kernel_pre_handleat priority 100 is where Drupal becomes Drupal. It callsDrupalKernel::preHandle(), which sanitizes the request, ensures a session object exists, loads every installed module, registers stream wrappers, and pushes the request onto the request stack. Code that runs outside this layer (like the page cache) cannot use module code; everything inside it can.
You can add your own middleware by registering a service with the http_middleware tag. Pick the priority deliberately: above 200 runs even for cached anonymous pages, below 100 runs with all modules loaded.
The kernel events
Section titled “The kernel events”Once the request reaches http_kernel.basic, Symfony’s HttpKernel::handle() drives the rest through events, in this order:
kernel.request. Subscribers prepare and route the request. Therouter_listenerservice asks the router to match the path. Drupal’s router is an\Drupal\Core\Routing\AccessAwareRouter. Matching includes route enhancers, which upcast route parameters: a{node}placeholder in the path becomes a loaded node object, not the string"42". The router then immediately runs the route’s access checks against those objects and throws an access-denied exception on failure. Matching a route and being allowed to use it are one step, not two.- Controller resolution. The kernel reads the
_controllerattribute that routing placed on the request, instantiates your controller through the class resolver (so dependency injection works), and passes the already-upcast route parameters to your method, so a{node}argument arrives as a loaded node object. See routes and controllers for the route definition side. kernel.controller. A last chance to inspect or replace the controller before it runs.- The controller runs. It returns either a
Responseor, far more commonly in Drupal, a render array (a structured PHP array describing output, not output itself). kernel.view, only when the controller did not return aResponse.\Drupal\Core\EventSubscriber\MainContentViewSubscriberpicks a main content renderer for the request format (HTML, Ajax, dialog) and turns the render array into a response. This event is the bridge between the request lifecycle and the render pipeline.kernel.response. Subscribers finalize the outgoing response.\Drupal\Core\EventSubscriber\FinishResponseSubscribersets theContent-Typeand language headers, applies cacheability headers, and decides whether the response is eligible for external caching.kernel.exception, whenever any of the above throws. Exception subscribers convert exceptions into responses: aNotFoundHttpExceptionbecomes the 404 page, anAccessDeniedHttpExceptionthe 403 page. An error page in Drupal is a normal response produced through this event, which is why 404 pages still have blocks and theming.kernel.terminate, after the response has been sent to the browser. Heavy cleanup work (cache writes, for example) runs here so it does not delay the visitor.
On kernel.request the router matches the route and the controller runs, returning a render array. On kernel.view the main content view subscriber turns that array into a response: the controller does not build the full page. On kernel.response the finish response subscriber sets headers and cacheability before the response leaves the kernel.
The response then travels back out through the middleware layers: the session middleware saves the session, the page cache middleware stores cacheable anonymous responses, and index.php sends the result.
How core uses it
Section titled “How core uses it”- The Page Cache module is “only” a middleware.
Drupal\page_cache\StackMiddleware\PageCacheis registered inpage_cache.services.ymlwith{ name: http_middleware, priority: 200 }. Its entire superpower, serving pages before Drupal boots modules, comes from its position in the stack, not from special treatment in the kernel. - Routing and access are fused.
\Drupal\Core\Routing\AccessAwareRouterwraps the plain router and calls the access manager insidematchRequest(). This guarantees no controller ever runs for a user who fails the route’srequirements. - Render arrays work because of one subscriber.
\Drupal\Core\EventSubscriber\MainContentViewSubscriberlistens tokernel.viewand renders whatever array your controller returned. Remove it and every Drupal controller returning a render array would cause an error. The lifecycle, not the controller, owns rendering. - Maintenance mode is an event subscriber.
Drupal\Core\EventSubscriber\MaintenanceModeSubscriberlistens tokernel.requestand replaces the page early when the site is offline, which is why maintenance mode does not depend on the requested route working.
To write your own code at these interception points, implement an event subscriber; the services and events guide shows the full workflow, and hooks and events explains when an event beats a hook.
Common misconceptions
Section titled “Common misconceptions”- “Drupal fully boots for every request.” No. For an anonymous page cache hit, the request turns around at the page cache middleware: modules are never loaded, hooks never fire, controllers never run. If your code must run on every request including cached ones, it has to be middleware above priority 200, or live outside Drupal (web server, content delivery network).
- “The controller builds the page.” The controller builds the main content as a render array. Blocks, the page template, attachments, and placeholdering are added later, during the
kernel.viewevent, by the main content renderer. That is also why a controller can return a bare array and still produce a full page. - “Routing finds the controller, then access is checked somewhere later.” Both happen inside route matching, in
AccessAwareRouter::matchRequest(). By the timekernel.requestfinishes, the user is already authorized for the route. - “A 404 means Drupal crashed before doing anything.” A 404 is a thrown
NotFoundHttpExceptionconverted into a themed response bykernel.exceptionsubscribers. The full pipeline runs for error pages, which is also why slow 404 handling can hurt performance. - “index.php contains bootstrap logic I might need to read.” It is a handful of lines. All behavior lives in
DrupalKernel, the middleware stack, and event subscribers, where you can alter it through services; nothing is hardcoded in the entry point.
Where to go next
Section titled “Where to go next”- Routes and controllers: define your own paths and respond to them.
- Render API and the theming pipeline: what happens to your render array after
kernel.view. - Caching: the metadata that decides whether the page cache and dynamic page cache may store a response.
- Hooks and events: how to subscribe to the kernel events described here.
- Services and dependency injection: the container that all of these moving parts are registered in.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.