One of the greatest strengths of Drupal is its sophisticated, multi-layered caching system, which is designed to keep even the most complex sites running fast. However, the most common moment of frustration for many developers is checking the browser network tab only to find the X-Drupal-Dynamic-Cache header returning UNCACHEABLE.
This status is not just a label. It is a signal that a specific component on your page is preventing Drupal from storing the response, forcing the server to rebuild the page from scratch for every single visitor. Understanding why this happens is the key to unlock true site performance and reduce unnecessary database and server strain.
Unlike the Internal Page Cache module, which stores entire HTML responses for anonymous users, the Internal Dynamic Page Cache module handles the page shel for both anonymous and authenticated users. It caches the common parts of a page while using placeholders for dynamic elements. This module is part of Drupal Core and is typically enabled by default in standard installations.
The X-Drupal-Dynamic-Cache header is your primary diagnostic window into this system. It typically returns one of the following values:
The quickest way to check headers without a browser is to use the curl command-line tool. Use the -I flag to fetch only the HTTP headers.
Example:
curl -I https://example.comIn the output, look for the X-Drupal-Dynamic-Cache line:
HTTP/2 200 OK
...
X-Drupal-Dynamic-Cache: HIT
...To inspect the header in a browser:
X-Drupal-Dynamic-Cache header.To debug an UNCACHEABLE status, you must understand the three properties of the $build array that the Drupal Renderer service tracks. These properties bubble up from the smallest component to the final page response.
Cache bubbling means that the least cacheable metadata from any single render element dictates the cacheability of the entire page. For example, a single element with max-age: 0 will bubble up and make the whole page response uncacheable.
user.permissions, url.query_args. High-cardinality contexts such as session or user often trigger the UNCACHEABLE status because the cache variation would be unique to every visitor.max-age: 0 acts as a global Kill Switch, bubbling up to invalidate the entire page cache.The X-Drupal-Dynamic-Cache header returns UNCACHEABLE when the Drupal Internal Dynamic Page Cache service determines that the response contains specific metadata or properties that make it ineligible for storage. This status is typically triggered by one of the following four core reasons, which identify where in the request-response lifecycle the caching was blocked or invalidated.
Reason | Technical Cause |
|---|---|
Poor cacheability | Triggered by a low max-age, a high-cardinality context such as user or session, or volatile tags that are invalidated frequently (for example, real-time counters, or recent activity) that they cause constant cache churn. |
No cacheability | The controller returned a response that is not an instance of |
Request policy | A dynamic_page_cache_request_policy service, such as a CLI request or an active session during a POST, denied caching. |
Response policy | A dynamic_page_cache_response_policy service, such as the page_cache_kill_switch, explicitly disabled the cache. |
The relationship between X-Drupal-Dynamic-Cache and the standard Cache-Control header is architectural: while the dynamic header reports on Drupal's internal processing, the Cache-Control header communicates those results to external layers such as browsers and CDNs.
Because Drupal's metadata bubbles up, any element that triggers an UNCACHEABLE status will automatically force the Cache-Control to no-cache or private. This ensures that volatile or personalized content—which Drupal has already decided it cannot safely store—is not accidentally cached by external edge servers and served to the wrong user.
Essentially, you can view the X-Drupal-Dynamic-Cache header as the internal cause and the Cache-Control header as the external effect. Resolving an internal uncacheable status is often the only way to unlock downstream caching at the CDN layer. When Drupal finally returns a HIT, it will simultaneously update the Cache-Control header to a public, long-lived max-age, allowing your edge infrastructure to take over the heavy lifting.
You can debug cacheability without additional modules by enabling core's built-in debugging parameters in your development.services.yml file. This allows you to inspect how a response is being cached using the following options:
http.response.debug_cacheability_headers: Adds X-Drupal-Cache-Tags, X-Drupal-Cache-Contexts, and X-Drupal-Cache-Max-Age headers to every response. Note that complex sites may exceed header size limits (approximately 8K), which can be mitigated using the Debug Cacheability Headers Split module.renderer.config.debug: Adds HTML comments around every rendered element showing its cache metadata.To enable these response header debugging options, add the following parameters to the development.services.yml file and rebuild Drupal’s cache:
parameters:
renderer.config:
required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions']
auto_placeholder_conditions:
max-age: 0
contexts: ['session', 'user']
tags: []
debug: true
http.response.debug_cacheability_headers: trueIf a block contains logic that relies on a specific user's data or active session, it attaches high-cardinality metadata that forces the page to be UNCACHEABLE (poor cacheability).
$user = \Drupal::currentUser(); to fetch a profile field. Because each UID is unique, Drupal cannot safely cache the page for other users.\Drupal::service('session')->get('recent_ids');. Since sessions are unique to every visitor, including anonymous users once a session starts, this attaches the session cache context.Calling \Drupal::service('page_cache_kill_switch')->trigger(); is a nuclear option often found in legacy code or modules handling sensitive form data. It results in UNCACHEABLE (response policy). This is common in:
Explicitly setting '#cache'=> ['max-age' => 0] in any render array (whether it's a block, a field formatter, or a small container) tells Drupal that the element is never cacheable.
Because cache metadata bubbles up, a single max-age: 0 on a single block will mark the entire page as uncacheable. This is often used by developers as a quick fix for dynamic content, but it should be avoided in favor of Lazy Builders, which allow the rest of the page to remain cached while only the dynamic element is re-rendered.
Search facet blocks, common in Search API or Facets module implementations, are frequent culprits for UNCACHEABLE responses. Because facets update their counts and states based on the current URL query parameters and the user's specific filter selection, they often carry high-cardinality cache contexts such as url.query_args.
The primary way to fix an UNCACHEABLE response without losing dynamic functionality is through Lazy Builders (Auto-placeholding). This technique allows Drupal to cache the majority of the page (the shell) while deferring the rendering of volatile components.
By moving dynamic logic into a callback, you isolate the poorly cacheable metadata. The main page response no longer sees the high-cardinality contexts or max-age: 0, allowing the Dynamic Page Cache to return a HIT.
Step 1: Create the Lazy Builder Service. Define your service in your_module.services.yml:
services:
your_module.lazy_builder:
class: \Drupal\your_module\YourLazyBuilder
arguments: ['@current_user']Step 2: Implement the Callback. Your class should return a render array containing only the dynamic content and its specific cache metadata.
public function renderDynamicElement($user_id) {
return [
'#markup' => $this->t('Hello, user %id', ['%id' => $user_id]),
'#cache' => [
'contexts' => ['user'],
],
];
}Step 3: Use the Placeholder in your Block.
$build['dynamic_content'] = [
'#lazy_builder' => [
'your_module.lazy_builder:renderDynamicElement',
[$user_id],
],
'#create_placeholder' => TRUE,
];If you prefer a configuration-based approach over custom code, these modules offer alternative solutions to facilitate cache optimization:
The UNCACHEABLE status in the X-Drupal-Dynamic-Cache header signals a critical performance bottleneck. It indicates that poorly cacheable metadata, such as a max-age: 0 or high-cardinality contexts like user or session, has bubbled up from a single component to invalidate the entire page response. This forces Drupal to execute a full PHP bootstrap and rendering cycle for every visitor, which dramatically increases database I/O and server strain.
To resolve this, the primary strategy is isolation through Lazy Builders (auto-placeholding). By refactoring dynamic logic into a Lazy Builder callback, you prevent the uncacheable metadata from polluting the main page response.
This isolation allows the surrounding page structure (the shell) to achieve a HIT status, restoring high-level caching at the Dynamic Page Cache layer. Ultimately, fixing the internal UNCACHEABLE status is the key to unlocking external caching and reducing database and server load.