Skip to content

ConceptFor module developersDrupal 11

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?”.

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.

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:

  1. The kernel boundary. \Drupal\Core\DrupalKernel boots the environment, loads settings.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.
  2. The middleware stack. The http_kernel service is a \Drupal\Core\StackMiddleware\StackedHttpKernel: a chain of services tagged http_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.
  3. The event-driven core. The innermost layer, http_kernel.basic, is Symfony’s HttpKernel. It does no work itself; it dispatches kernel events, and event subscribers (services that listen for events) do the routing, access checking, and rendering.
The request as an onion

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.

Middleware classes implement Symfony’s HttpKernelInterface and wrap each other. These are the layers core registers, from outermost in:

PriorityServicePurpose
500http_middleware.ajax_page_stateExpands the compressed ajax_page_state query parameter
400http_middleware.negotiationSets the request format from the _format query parameter
300http_middleware.reverse_proxyApplies reverse proxy settings from settings.php
250http_middleware.corsHandles cross-origin requests when CORS is enabled
200http_middleware.page_cacheServes the anonymous page cache (Page Cache module)
150http_middleware.big_pipeContent-Length handling for streamed BigPipe responses
140http_middleware.content_lengthAdds the Content-Length header to responses
100http_middleware.kernel_pre_handleCalls back into the kernel to finish bootstrapping
50http_middleware.sessionStarts 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_handle at priority 100 is where Drupal becomes Drupal. It calls DrupalKernel::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.

Once the request reaches http_kernel.basic, Symfony’s HttpKernel::handle() drives the rest through events, in this order:

  1. kernel.request. Subscribers prepare and route the request. The router_listener service 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.
  2. Controller resolution. The kernel reads the _controller attribute 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.
  3. kernel.controller. A last chance to inspect or replace the controller before it runs.
  4. The controller runs. It returns either a Response or, far more commonly in Drupal, a render array (a structured PHP array describing output, not output itself).
  5. kernel.view, only when the controller did not return a Response. \Drupal\Core\EventSubscriber\MainContentViewSubscriber picks 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.
  6. kernel.response. Subscribers finalize the outgoing response. \Drupal\Core\EventSubscriber\FinishResponseSubscriber sets the Content-Type and language headers, applies cacheability headers, and decides whether the response is eligible for external caching.
  7. kernel.exception, whenever any of the above throws. Exception subscribers convert exceptions into responses: a NotFoundHttpException becomes the 404 page, an AccessDeniedHttpException the 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.
  8. 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.
Kernel events in order

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.

  • The Page Cache module is “only” a middleware. Drupal\page_cache\StackMiddleware\PageCache is registered in page_cache.services.yml with { 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\AccessAwareRouter wraps the plain router and calls the access manager inside matchRequest(). This guarantees no controller ever runs for a user who fails the route’s requirements.
  • Render arrays work because of one subscriber. \Drupal\Core\EventSubscriber\MainContentViewSubscriber listens to kernel.view and 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\MaintenanceModeSubscriber listens to kernel.request and 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.

  • “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.view event, 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 time kernel.request finishes, the user is already authorized for the route.
  • “A 404 means Drupal crashed before doing anything.” A 404 is a thrown NotFoundHttpException converted into a themed response by kernel.exception subscribers. 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.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.