
Fix Drupal Cache Issues: Tags, Contexts & Debugging Guide
Drupal's cache system is one of the most misunderstood parts of the platform. You've probably seen it: a client reports that their homepage still shows outdated content after they published changes, or a custom block displays the same data to every visitor regardless of context. These problems almost always trace back to how Drupal handles cacheability metadata.
The good news is that once you understand the underlying mechanics, most cache-related bugs become predictable and fixable. This article breaks down how Drupal's cache actually works, why anonymous users see different behavior than authenticated ones, and what to do when your custom blocks refuse to update. If you've experienced Drupal cache not working as expected, you're in the right place.
The Three Pillars of Drupal Caching
Drupal's cache system relies on three types of metadata that work together to determine what gets cached, how long it stays cached, and when it gets invalidated. Understanding Drupal cacheability metadata is essential for resolving common Drupal cache issues.
Cache Tags
Cache tags identify what data a cached item depends on. When that data changes, Drupal knows which cached items need to be cleared.
Examples:
- node:5 - invalidates when node 5 is updated
- user:12 - invalidates when user 12 changes
- config:system.site - invalidates when site configuration changes
- node_list - invalidates when any node is created or deleted
When Drupal cache tags not invalidating becomes an issue, it's typically because the correct tags weren't declared on the render array.
Cache Contexts
Cache contexts define variations of the same content. If a block should display differently based on user role or current URL, you declare that with contexts.
Common contexts:
- user - varies per individual user
- user.roles - varies by user role
- url.path - varies by current page path
- url.query_args - varies by query string parameters
Cache Max-Age
Max-age sets time-based expiration in seconds:
- 3600 - expires after one hour
- 0 - not cacheable at all
- -1 (or Cache::PERMANENT) - cached until invalidated by tags
How Cache Bubbling Works
Here's where things get tricky. Drupal uses "cache bubbling" to propagate cacheability metadata up through the render tree. When a child element has specific cache requirements, those requirements bubble up to parent elements and ultimately affect the entire page. Drupal cache bubbling directly impacts the Drupal render cache behavior across your entire page.
Picture it like this:
Page (inherits all child cacheability)
└── Region
└── Block (cache contexts: user.roles)
└── View (cache tags: node_list)
└── Node teaser (cache tags: node:123)Every cache tag, context, and the most restrictive max-age from child elements bubbles up to the page level. This is usually helpful. It means the page automatically knows when to invalidate based on its contents. But it also means a single poorly-configured element can make an entire page uncacheable.
A common pitfall: if any nested element sets max-age: 0, that can bubble up and prevent the whole page from being cached. Working with teams has taught us that this single issue accounts for a large percentage of "my site is slow" complaints.
The Anonymous User Problem
The Drupal anonymous user cache problem is one of the most frequently encountered issues in site development.
Drupal has two separate page caching systems, and understanding the difference explains most anonymous user caching issues. Understanding Drupal page cache vs dynamic page cache is key to resolving these problems.
Internal Page Cache
The Page Cache module stores complete HTML pages for anonymous users. It works under one key assumption: anonymous pages are identical for all visitors.
This assumption causes problems when your site needs to show different content to anonymous visitors: shopping carts, location-based content, or time-sensitive displays. Even if your block correctly sets cache metadata, the Page Cache may still serve a cached full-page version.
If your site needs personalized content for anonymous users, you have two options:
- Disable Internal Page Cache and handle caching differently
- Deliver personalized content via JavaScript or AJAX after the page loads
Dynamic Page Cache
The Dynamic Page Cache works differently. It caches pages with placeholders, then fills in the dynamic parts on each request. This handles authenticated users and pages with user-specific content.
The system automatically detects poorly cacheable elements (like those with max-age: 0 or high-cardinality contexts like user or session) and replaces them with placeholders. This prevents those elements from making the entire page uncacheable.
Custom Block Caching: Getting It Right
Proper Drupal custom block cache configuration is critical for both performance and content accuracy.
Most custom block caching issues come from not declaring cacheability metadata properly. Here's what a well-configured custom block looks like:
public function build() {
$build = [
'#markup' => $this->t('Dynamic content here'),
'#cache' => [
'tags' => ['node_list', 'custom_block:example'],
'contexts' => ['url.path', 'user.roles'],
'max-age' => 3600,
],
];
return $build;
}
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['node_list']);
}
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['url.path']);
}Key points:
- Declare cache tags for every piece of data your block depends on
- Add contexts for any condition that should produce different output
- Set max-age only if time-based expiration makes sense for your use case
When to Use Lazy Builders
For truly dynamic content that shouldn't affect page cacheability, use lazy builders:
$build['dynamic_part'] = [
'#lazy_builder' => ['my_module.lazy_builder:build', [$arg1, $arg2]],
'#create_placeholder' => TRUE,
];The above Drupal lazy builder example demonstrates the pattern for isolating uncacheable content from the rest of the page.
This approach works well for:
- User-specific content on otherwise cacheable pages
- Real-time data like stock prices or live scores
- Session-dependent content
Views Caching Options
Configuring Drupal Views cache settings correctly can significantly impact your site's Drupal performance optimization.
Views offers three caching approaches:
- None - Development only, or data that changes constantly
- Tag-based - Most production use cases, invalidates when displayed content changes
- Time-based - External data sources, or when you need predictable refresh intervals
We've found that tag-based caching works best for most sites. It keeps content fresh without unnecessary cache clears, and it integrates naturally with Drupal's entity system.
Common Views Cache Gotchas
Exposed filters not varying: Add the url.query_args cache context to your View if you use exposed filters.
Relationship data not invalidating: Related entity cache tags don't always bubble correctly. You may need to add them manually.
Time-relative filters: Views with "upcoming events" or "articles from the last week" filters won't automatically expire at the right time. This is a known limitation (Core Issue #2352009) that's been discussed for years. You may need a custom approach to force appropriate TTL behavior.
Debugging Cache Issues
Effective Drupal cache debugging requires understanding the available tools and what they reveal.
When something isn't caching correctly, start with these steps:
Enable Debug Headers
Add this to your development.services.yml:
parameters:
http.response.debug_cacheability_headers: trueThen check response headers in your browser's developer tools. The X-Drupal-Cache headers provide immediate visibility into cache behavior:
- X-Drupal-Cache - HIT or MISS for Page Cache
- X-Drupal-Dynamic-Cache - HIT, MISS, or UNCACHEABLE
- X-Drupal-Cache-Tags - All cache tags affecting the response
- X-Drupal-Cache-Contexts - All cache contexts in play
Useful Drush Commands
drush cr # Rebuild all caches
drush cache:get [cid] [bin] # Inspect specific cache entriesContributed Debugging Tools
- WebProfiler - Symfony-style profiler toolbar showing cache information
- Cache Review - Audits cacheability issues across your site
- Renderviz - Visualizes render cache boundaries
We recommend starting with WebProfiler during development; it surfaces cache information that would otherwise require header inspection and log analysis.
HTMX: A New Option for Dynamic Content
Drupal 11.2 added HTMX as a core dependency, with full integration landing in version 11.3. This gives you another option for handling dynamic content without compromising page cacheability. Drupal HTMX dynamic content integration offers a modern approach to this challenge.
The approach is simple: cache the full page, then use HTMX to fetch small dynamic fragments via HTTP requests. This reduces the need for lazy builders in some cases and simplifies the mental model for handling dynamic content on cached pages.
For sites where you've struggled with lazy builder complexity or Drupal BigPipe configuration, HTMX offers a straightforward alternative worth exploring.
Quick Decision Guide
Use Page Cache when:
- Serving anonymous traffic
- Pages are identical for all anonymous visitors
- You need maximum performance
Disable Page Cache when:
- Anonymous visitors see personalized content
- You rely on per-session or geo-based variations
Use tag-based Views caching when:
- Displaying Drupal entities
- Content changes unpredictably
- Freshness matters more than predictable timing
Use lazy builders when:
- Small parts of a page need user-specific data
- You want the rest of the page to remain cacheable
- Real-time data display is required
Consider HTMX when:
- You want to avoid JavaScript complexity
- Progressive enhancement matters
- Dynamic fragments are distinct from the main page content
Conclusion
Drupal's cache system requires understanding its mechanics to use correctly. The combination of cache tags, contexts, max-age, and bubbling behavior gives you fine-grained control, but also plenty of opportunities for misconfiguration.
Most common issues trace back to missing cache metadata in custom code or misunderstanding how the Page Cache treats anonymous users differently from authenticated ones. Start debugging with response headers, add proper cacheability declarations to your render arrays, and consider lazy builders or HTMX when you need dynamic content on otherwise cacheable pages.
If your Drupal site has persistent caching issues affecting performance or content freshness, diagnosing the root cause often requires examining cache metadata across multiple layers. We can help you audit your site's cacheability configuration and identify exactly where the cache chain is breaking down.
