---
title: "Dynamic Cache Debugging Playbook"
date: "2026-04-21T01:28:24+00:00"
summary:
image:
type: "article"
url: "/acquia-cloud-platform/help/97261-dynamic-cache-debugging-playbook"
id: "3e7558a8-b70e-4ae8-98cb-7554964f381f"
---

Table of contents will be added

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.

What is the Dynamic Page Cache?
-------------------------------

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.

### Understanding header values

The `X-Drupal-Dynamic-Cache` header is your primary diagnostic window into this system. It typically returns one of the following values:

*   **HIT:** The request was served directly from the cache. This is the optimal state, resulting in the fastest response times and minimal server overhead.
*   **MISS:** The request was not found in the cache and required a full rendering cycle. However, this response is eligible for caching and should ideally result in a HIT on the next request.
*   **UNCACHEABLE:** The request contains specific metadata or logic, such as high-cardinality contexts or a max-age of 0, that makes it ineligible for storage. This forces a full PHP bootstrap and rendering cycle for every single visitor.

### How to check the Dynamic Cache header

#### Using curl (command line)

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.com

In the output, look for the `X-Drupal-Dynamic-Cache` line:

    HTTP/2 200 OK
    ...
    X-Drupal-Dynamic-Cache: HIT
    ...

#### Using browser DevTools (Network tab)

To inspect the header in a browser:

1.  Open your website in the browser.
2.  Open Developer Tools.
3.  Navigate to the **Network** tab.
4.  Refresh the page.
5.  Click on the first entry in the list, which is usually the main document request.
6.  In the Headers sub-pane, scroll down to the **Response Headers** section.
7.  Locate the `X-Drupal-Dynamic-Cache` header.

### Three pillars of cacheability

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.

*   **Cache Contexts (Variations):** These define what the content varies by. For example, `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.
*   **Cache Tags (Invalidation):** These identify data dependencies such as node:5. If the underlying data is changed, the specific cache entries are purged.
*   **Max-Age (Time):** The duration a component remains valid. A `max-age: 0` acts as a global _Kill Switch_, bubbling up to invalidate the entire page cache.

Decoding UNCACHEABLE: four core reasons
---------------------------------------

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 `CacheableResponseInterface`, which means that it lacks necessary metadata.

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 downstream impact: Cache-Control headers
--------------------------------------------

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.

Identifying the culprit: Debugging workflows
--------------------------------------------

### The "No-Module" core debugger

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](https://www.drupal.org/project/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: true

Common culprits
---------------

### Session and user context dependencies

If 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 context example: Using `$user = \Drupal::currentUser();` to fetch a profile field. Because each UID is unique, Drupal cannot safely cache the page for other users.
*   Session context example: Implementing a "Recently Viewed Products" block that pulls IDs from `\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.

### Cache suppression trigger

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:

*   Search pages: Ensuring real-time results often bypasses the cache.
*   Form errors: To prevent one user's validation errors from being cached and shown to the next visitor.
*   Redirects: Logic based on volatile, non-cacheable backend states.

### Max-age _Kill Switch_

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.

### Volatile search facets

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.

*   Problem: On a complex search page with dozens of facet combinations, the number of possible cache variations becomes astronomical. To prevent cache-fill attacks or storage exhaustion, Drupal may automatically mark these blocks and subsequently the entire page as uncacheable.
*   Fix: Use the [Facets](https://www.drupal.org/project/facets) module's built-in support for AJAX or utilize a Lazy Builder to ensure the main search results remain cached while the facet links and counts are updated dynamically.

Strategies for restoration: Lazy Builders & Decoupling
------------------------------------------------------

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.

### Why use Lazy Builders?

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-by-step implementation

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,
    ];

### Contributed module solutions

If you prefer a configuration-based approach over custom code, these modules offer alternative solutions to facilitate cache optimization:

*   [Views Block with Lazy Builder](https://www.drupal.org/project/views_block_with_lazy_builder): Specifically designed to turn any Views block into a lazy-loaded placeholder, preventing complex views from tanking page performance.
*   [Block AJAX](https://www.drupal.org/project/block_ajax): Allows you to load blocks via AJAX after the initial page load, which is excellent for high-invalidation data like real-time stats or ads.
*   [HTMX Extras](https://www.drupal.org/project/htmx_extras): Provides lightweight, reactive features using HTMX for lazy loading specific entity renders and route contents. It also includes a Search API lazy load row plugin for rendering Views rows as they enter the user's viewport.

Summary
-------

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.

References
----------

*   [Page Cache & Dynamic Page Cache modules' response headers improved](https://www.drupal.org/node/2958442)
*   [Dynamic Page Cache Overview](https://www.drupal.org/docs/8/core/modules/dynamic-page-cache/overview)
*   [Cache Contexts API](https://www.drupal.org/docs/develop/drupal-apis/cache-api/cache-contexts)
*   [Auto-placeholding Guide](https://www.drupal.org/docs/drupal-apis/render-api/auto-placeholdering)
*   [Cacheability of Render Arrays](https://www.drupal.org/docs/drupal-apis/render-api/cacheability-of-render-arrays)
*   [Cache Max-Age API](https://www.drupal.org/docs/develop/drupal-apis/cache-api/cache-max-age)
*   [Cache Tags API](https://www.drupal.org/docs/develop/drupal-apis/cache-api/cache-tags)