drupal-dynamic-cache
Dynamic Page Cache & BigPipe
These modules cache pages for authenticated users while handling personalized content via placeholders.
When to Use
- Improving performance for logged-in users
- Debugging UNCACHEABLE responses
- Implementing personalized content that doesn't break caching
- Understanding BigPipe streaming behavior
How Dynamic Page Cache Works
Request → Check cache
├── HIT: Serve cached structure → Replace placeholders → Response
└── MISS: Render page → Auto-placeholder dynamic parts → Cache → Response
Response Headers
| Header | Values | Meaning |
|---|---|---|
X-Drupal-Dynamic-Cache |
HIT |
Served from cache |
MISS |
Rendered and cached | |
UNCACHEABLE |
Cannot cache (max-age:0) |
Auto-Placeholder Conditions
Content is automatically placeholdered when:
max-age: 0- Cache context
userorsession #create_placeholderis TRUE
Examples
Example 1: Check Dynamic Cache Status
Input: "How do I know if Dynamic Page Cache is working?"
Output:
# Check response header
curl -sI -H "Cookie: SESS123=abc" https://site.com/ | grep X-Drupal-Dynamic-Cache
# Expected results:
# X-Drupal-Dynamic-Cache: HIT → Working, served from cache
# X-Drupal-Dynamic-Cache: MISS → First request, now cached
# X-Drupal-Dynamic-Cache: UNCACHEABLE → Problem! Something has max-age:0
Example 2: Debug UNCACHEABLE Response
Input: "My page shows UNCACHEABLE, how do I fix it?"
Output:
// Enable debug headers to see what's wrong
// settings.local.php
$settings['http.response.debug_cacheability_headers'] = TRUE;
// Check headers:
// X-Drupal-Cache-Max-Age: 0 → Something set max-age:0
// Common culprits:
// 1. Block with max-age:0
// 2. Form without lazy builder
// 3. Custom module not setting cache metadata
// Find the source with Xdebug breakpoint:
// File: Cache.php, Method: mergeMaxAges(), Condition: $a === 0 || $b === 0
Example 3: Lazy Builder for User Content
Input: "I need to show user-specific data without breaking cache"
Output:
// DON'T do this - breaks page caching:
$build['greeting'] = [
'#markup' => 'Hello ' . $user->getDisplayName(),
'#cache' => ['contexts' => ['user']], // Page becomes per-user!
];
// DO this - use lazy builder:
$build['greeting'] = [
'#lazy_builder' => ['my_module.lazy:userGreeting', [$user->id()]],
'#create_placeholder' => TRUE,
];
// LazyBuilders service
class LazyBuilders implements TrustedCallbackInterface {
public static function trustedCallbacks() {
return ['userGreeting'];
}
public function userGreeting(int $uid): array {
$user = User::load($uid);
return [
'#markup' => 'Hello ' . $user->getDisplayName(),
'#cache' => [
'contexts' => ['user'],
'tags' => ['user:' . $uid],
],
];
}
}
Example 4: BigPipe Streaming
Input: "How does BigPipe work with placeholders?"
Output:
<!-- Initial HTML sent immediately: -->
<div data-big-pipe-placeholder-id="callback=my_module.lazy%3AuserGreeting&args[0]=5">
<span class="placeholder">Loading...</span>
</div>
<!-- Later, streamed via script tag: -->
<script type="application/vnd.drupal-ajax">
[{"command":"insert",
"selector":"[data-big-pipe-placeholder-id='callback=my_module.lazy%3AuserGreeting&args[0]=5']",
"data":"Hello John!"}]
</script>
<!-- JavaScript replaces placeholder content automatically -->
Example 5: Force Placeholder Creation
Input: "I want to placeholder content even without user context"
Output:
$build['dynamic'] = [
'#lazy_builder' => ['my_module.lazy:dynamicContent', []],
'#create_placeholder' => TRUE, // Force placeholder regardless of contexts
];
Example 6: Configure Auto-Placeholder Conditions
Input: "How do I customize what gets auto-placeholdered?"
Output:
# services.yml
parameters:
renderer.config:
auto_placeholder_conditions:
max-age: 0
contexts:
- 'session'
- 'user'
- 'my_custom_context' # Add custom context
tags: [] # Tags that trigger placeholder (usually empty)
Example 7: BigPipe Requirements
Input: "BigPipe isn't working, what could be wrong?"
Output:
# Check server configuration:
# Nginx - disable buffering (auto-handled via X-Accel-Buffering header)
location ~ \.php$ {
# BigPipe sends: X-Accel-Buffering: no
# Nginx respects this automatically
}
# Apache - works by default
# Common issues:
# 1. Output buffering in PHP - check ob_start() calls
# 2. Reverse proxy buffering - configure Varnish/CDN
# 3. JavaScript disabled - falls back to non-streaming
# Verify BigPipe is enabled
drush pm:list | grep big_pipe
Example 8: Anonymous vs Authenticated Caching
Input: "Explain the caching difference between anonymous and logged-in users"
Output:
Anonymous User:
┌─────────────────────────────────────────┐
│ Page Cache → HIT → Full page served │
│ (Dynamic Page Cache skipped) │
│ No placeholders, no BigPipe │
└─────────────────────────────────────────┘
Authenticated User:
┌─────────────────────────────────────────┐
│ Page Cache → SKIP (has session cookie) │
│ Dynamic Page Cache → HIT/MISS │
│ Placeholders replaced via BigPipe │
└─────────────────────────────────────────┘
// Check with curl:
// Anonymous
curl -sI https://site.com/ | grep X-Drupal
// X-Drupal-Cache: HIT
// Authenticated (with session cookie)
curl -sI -H "Cookie: SESSabc=xyz" https://site.com/ | grep X-Drupal
// X-Drupal-Dynamic-Cache: HIT
Common Mistakes
| Mistake | Impact | Solution |
|---|---|---|
| max-age:0 without lazy builder | Page UNCACHEABLE | Use #lazy_builder |
user context on blocks |
Per-user cache entries | Use user.roles or lazy builder |
| Disabling Dynamic Page Cache | Slow authenticated pages | Fix underlying max-age issues |
| Object args to lazy builder | Runtime error | Use scalar values only |
Debugging Checklist
# 1. Check Dynamic Cache status
curl -sI -H "Cookie: SESS=x" https://site.com/ | grep X-Drupal-Dynamic-Cache
# 2. Enable debug headers
# settings.local.php: $settings['http.response.debug_cacheability_headers'] = TRUE;
# 3. Check max-age
curl -sI https://site.com/ | grep X-Drupal-Cache-Max-Age
# 4. Verify BigPipe module
drush pm:list | grep big_pipe
More from sparkfabrik/sf-awesome-copilot
drupal-cache-debugging
Drupal cache debugging techniques and troubleshooting workflows. Use when asked about X-Drupal-Cache headers interpretation, finding max-age 0 sources, WebProfiler usage, cache hit/miss analysis, stale content debugging, or performance profiling cache-related issues.
19drupal-cache-contexts
Drupal cache contexts implementation guide. Use when asked about request-based cache variations, user.roles vs user context, URL contexts, language contexts, custom cache contexts, or cache context hierarchy. Helps prevent cache explosion from overly broad contexts.
19drupal-cache-tags
Drupal cache tags implementation guide. Use when asked about cache tag naming conventions, entity tags, list tags, custom tags, tag invalidation strategies, or debugging tag-based cache invalidation issues. Covers node:ID, config:name, entity_list patterns.
17drupal-lazy-builders
Drupal lazy builders and placeholder implementation. Use when asked about #lazy_builder render array property, TrustedCallbackInterface, auto-placeholdering, BigPipe integration, personalized content caching, or how to make user-specific content cacheable.
17drupal-cache-maxage
Drupal cache max-age configuration and behavior. Use when asked about time-based cache expiration, Cache::PERMANENT, max-age 0 issues, why Page Cache ignores max-age, or when content appears stale despite time expiration. Critical for understanding caching layer differences.
16http-cache-tools
HTTP cache debugging tools and techniques. Use when asked to inspect cache headers, debug HTTP responses, use curl for cache analysis, or verify caching behavior. Includes SparkFabrik container context with make drupal-cli and docker compose commands.
11