Debug Non-Cacheable Blocks in Magento 2 Full Page Cache
A single non-cacheable block in your Magento 2 layout can destroy Full Page Cache (FPC) performance for entire page types. When Magento detects even one block marked as cacheable="false", it disables FPC for the entire page—forcing dynamic PHP generation for every request instead of serving cached HTML.
This guide provides production-tested tools and techniques to identify non-cacheable blocks, understand their performance impact, and fix or work around them. These methods have helped optimize dozens of high-traffic Magento stores where FPC hit rates dropped from 95%+ to under 10% due to a single poorly-configured third-party extension.
Why Non-Cacheable Blocks Matter
Full Page Cache (FPC) is Magento’s most critical performance optimization. When working properly:
- Cached pages serve in ~50-100ms (vs 2-5 seconds dynamic generation)
- Application servers handle 10-20x more traffic
- Database load decreases by 90%+
- Customer experience improves dramatically
But FPC operates on an all-or-nothing basis at the page level:
- ✅ All blocks cacheable → Entire page cached → Fast
- ❌ One block non-cacheable → Entire page dynamic → Slow
Common symptoms of non-cacheable block issues:
- High Time to First Byte (TTFB) on specific page types (category, product, CMS)
- Varnish showing low cache hit rates for specific URLs
- Application servers at high CPU despite “FPC enabled”
X-Magento-Cache-Debug: MISSheaders on every request- Slow page loads that don’t improve with repeated visits
Understanding Magento’s Caching Logic
Magento determines page cacheability during layout rendering:
// Simplified flow in Magento\Framework\View\Layout
foreach ($layoutElements as $element) {
if ($element->getAttribute('cacheable') === 'false') {
$pageCacheable = false;
break; // One non-cacheable block ruins everything
}
}
Important: This check happens after layout XML merging, so:
- Third-party modules can mark your blocks as non-cacheable
- Plugins/observers can modify block cacheability at runtime
- Layout updates from database (CMS widgets) can disable caching
Common Sources of Non-Cacheable Blocks
From analyzing 50+ Magento installations, here are the most frequent culprits:
| Source | Frequency | Typical Reason |
|---|---|---|
| Third-party extensions | 60% | Poor development practices, lazy implementation |
| Custom development | 25% | Misunderstanding of caching architecture |
| Widgets with dynamic content | 10% | CMS widgets injecting user-specific data |
| Poorly configured modules | 5% | Legitimate use cases configured incorrectly |
Extension Categories Most Likely to Cause Issues:
- Customer-specific content modules: Recently viewed, wishlists, comparison tools
- Dynamic pricing extensions: Customer group pricing, time-based discounts
- Personalization modules: Recommendations, targeted content
- Analytics/tracking tools: Session-based event tracking
- A/B testing platforms: Server-side variant selection
Method 1: Find Non-Cacheable Blocks via XML Search
The fastest way to identify non-cacheable blocks is searching layout XML files directly.
Search All Layout Files
# Search entire codebase (including Magento core)
find vendor app/code app/design -name "*.xml" -exec grep -li 'cacheable.*false' {} +
# Search only third-party and custom code (recommended)
find vendor app/code app/design \
-not \( -path vendor/magento -prune \) \
-name "*.xml" \
-exec grep -Hi 'cacheable.*false' {} +
Enhanced Search with Context
Get the surrounding lines to understand which blocks are affected:
# Show 3 lines before and after each match
find vendor app/code app/design \
-not \( -path vendor/magento -prune \) \
-name "*.xml" \
-exec grep -Hi -B3 -A3 'cacheable.*false' {} +
Example Output:
vendor/acme/magento2-recommendations/view/frontend/layout/catalog_product_view.xml:
<referenceContainer name="product.info.main">
<block class="Acme\Recommendations\Block\Product\Related"
name="acme.product.recommendations"
cacheable="false" <!-- ❌ FOUND IT -->
template="Acme_Recommendations::product/related.phtml"/>
</referenceContainer>
Now you know:
- Module:
Acme_Recommendations - Layout file:
catalog_product_view.xml - Block name:
acme.product.recommendations - Impact: Product pages cannot be cached
Search by Layout Handle
If you know specific pages are slow, target those layout handles:
# Search category pages
grep -r "cacheable.*false" vendor/*/view/frontend/layout/catalog_category_view*.xml
# Search product pages
grep -r "cacheable.*false" vendor/*/view/frontend/layout/catalog_product_view*.xml
# Search CMS pages
grep -r "cacheable.*false" vendor/*/view/frontend/layout/cms_page_view*.xml
Method 2: Runtime Logging (Catch Dynamic Changes)
Some modules set cacheability via PHP plugins or observers rather than XML. This method catches those cases.
Create a Patch File
Use Composer’s cweagans/composer-patches to inject logging into Magento’s core layout processor.
composer.json:
{
"require": {
"cweagans/composer-patches": "^1.7"
},
"extra": {
"patches": {
"magento/framework": {
"Log non-cacheable blocks for debugging": "patches/log-noncacheable-blocks.patch"
}
}
}
}
patches/log-noncacheable-blocks.patch:
--- vendor/magento/framework/View/Layout.php
+++ vendor/magento/framework/View/Layout.php
@@ -1128,7 +1128,10 @@ class Layout extends \Magento\Framework\View\Layout\Element implements \Magento
$blockName = $element->getBlockName();
if ($blockName !== false && $this->structure->hasElement($blockName)) {
$cacheable = false;
- break;
+ $this->logger->warning('Non-cacheable block detected', [
+ 'block_name' => $blockName,
+ 'handles' => $this->getUpdate()->getHandles()
+ ]);
}
}
Apply the patch:
composer install # Applies patches automatically
Monitor logs:
tail -f var/log/system.log | grep "Non-cacheable"
Example log output:
[2026-01-24 12:34:56] main.WARNING: Non-cacheable block detected {"block_name":"acme.product.recommendations","handles":["default","catalog_product_view","catalog_product_view_id_123"]}
This tells you:
- Which block is non-cacheable
- Which page handles are affected
- Runtime context when the decision was made
Benefits of Logging Method:
✅ Catches blocks marked non-cacheable via PHP (not just XML) ✅ Shows when cacheability decisions happen ✅ Works in production (logs don’t impact frontend performance) ✅ Identifies issues from plugins/observers modifying layouts
Cleanup After Debugging:
Once you’ve identified the culprits, remove the patch to avoid log noise:
# Remove patch from composer.json, then:
composer install
Method 3: Visual On-Page Debugging
For rapid frontend debugging, inject a visual indicator directly on the page showing all non-cacheable blocks.
Add to Your Theme Template
Place this snippet in your theme’s footer.phtml or any template that loads on every page (temporarily):
app/design/frontend/YourVendor/YourTheme/Magento_Theme/templates/html/footer.phtml:
<?php
/**
* TEMPORARY DEBUG CODE - Remove after identifying non-cacheable blocks
*
* This renders a fixed-position debug panel showing all blocks marked cacheable="false"
* in the current page layout. Useful for quickly identifying FPC issues without
* checking logs or XML files.
*/
$layout = $this->getLayout();
// Use reflection to access protected XML property
$reflection = new ReflectionMethod(get_class($layout), 'getXml');
$reflection->setAccessible(true);
$xml = $reflection->invoke($layout);
// Find all blocks with cacheable="false"
$nonCacheableBlocks = $xml->xpath(
'//' . \Magento\Framework\View\Layout\Element::TYPE_BLOCK . '[@cacheable="false"]'
);
$blockCount = count($nonCacheableBlocks);
// Only render debug panel if non-cacheable blocks exist
if ($blockCount > 0): ?>
<div style="
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999999;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 20px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
border-top: 3px solid #c92a2a;
max-height: 300px;
overflow-y: auto;
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h3 style="margin: 0; font-size: 18px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px;">
⚠️ FPC Disabled - <?= $blockCount ?> Non-Cacheable Block<?= $blockCount > 1 ? 's' : '' ?>
</h3>
<button onclick="this.parentElement.parentElement.remove()" style="
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
">Close</button>
</div>
<div style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 6px; line-height: 1.8;">
<p style="margin: 0 0 10px 0; opacity: 0.9; font-size: 12px;">
This page <strong>cannot be cached</strong> due to the following blocks:
</p>
<?php foreach ($nonCacheableBlocks as $block):
$blockName = (string)$block->getBlockName();
$blockClass = (string)$block['class'];
$blockTemplate = (string)$block['template'];
?>
<div style="
background: rgba(255,255,255,0.1);
margin-bottom: 8px;
padding: 10px;
border-left: 4px solid #fff;
border-radius: 3px;
">
<div style="font-weight: bold; margin-bottom: 4px;">
📦 <?= $escaper->escapeHtml($blockName) ?>
</div>
<?php if ($blockClass): ?>
<div style="opacity: 0.8; font-size: 11px; margin-bottom: 2px;">
Class: <?= $escaper->escapeHtml($blockClass) ?>
</div>
<?php endif; ?>
<?php if ($blockTemplate): ?>
<div style="opacity: 0.8; font-size: 11px;">
Template: <?= $escaper->escapeHtml($blockTemplate) ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.2); font-size: 11px; opacity: 0.8;">
💡 <strong>Impact:</strong> This page generates dynamically on every request (~2-5s TTFB vs ~50-100ms cached)
</div>
</div>
<?php endif; ?>
What This Shows:
When you visit any page with non-cacheable blocks, you’ll see a prominent red banner at the bottom showing:
- Count of non-cacheable blocks
- Block names (so you know what to search for)
- PHP class names (identifies the responsible module)
- Template paths (helps locate the code)
Benefits:
✅ Instant visual feedback (no need to check logs) ✅ Shows all non-cacheable blocks on the page ✅ Includes block metadata for quick identification ✅ Can be used by QA team members without CLI access ✅ Works across all page types automatically
Important: Remove After Debugging
This method uses reflection to access protected properties, which has minor performance overhead. Once you’ve identified the problematic blocks, remove this code from your templates.
Fixing Non-Cacheable Blocks
Once identified, you have several remediation strategies:
Strategy 1: Make the Block Cacheable (Best)
If the block can be cached (doesn’t contain user-specific data), override the layout XML:
app/design/frontend/YourVendor/YourTheme/Vendor_Module/layout/catalog_product_view.xml:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<body>
<referenceBlock name="acme.product.recommendations" cacheable="true"/>
</body>
</page>
Validation: Check that the block actually works when cached. If it displays stale data, this approach won’t work.
Strategy 2: Use Private Content (ESI/AJAX)
For blocks that must be user-specific, use Magento’s private content system to load them client-side after page cache:
Convert to private content section:
<!-- Remove the non-cacheable server-side block -->
<referenceBlock name="acme.product.recommendations" remove="true"/>
<!-- Add cacheable placeholder that loads via AJAX -->
<block class="Magento\Framework\View\Element\Template"
name="acme.product.recommendations.placeholder"
template="Vendor_Module::ajax-placeholder.phtml"/>
ajax-placeholder.phtml:
<div data-bind="scope: 'acmeRecommendations'">
<!-- ko if: isLoading -->
<div class="loading">Loading recommendations...</div>
<!-- /ko -->
<!-- ko if: !isLoading -->
<div data-bind="html: content"></div>
<!-- /ko -->
</div>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"acmeRecommendations": {
"component": "Vendor_Module/js/recommendations",
"ajaxUrl": "<?= $block->getUrl('acme/ajax/recommendations') ?>"
}
}
}
}
}
</script>
This allows the page HTML to be cached while user-specific content loads asynchronously.
Strategy 3: Move to Customer Data Section
Use Magento’s built-in customer-data.js for user-specific data:
define([
'Magento_Customer/js/customer-data'
], function (customerData) {
var recommendations = customerData.get('acme-recommendations');
recommendations.subscribe(function (data) {
// Update UI with user-specific recommendations
});
});
Register the section in etc/frontend/sections.xml:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<action name="catalog/product/view">
<section name="acme-recommendations"/>
</action>
</config>
Strategy 4: Disable the Module (Last Resort)
If the module isn’t critical and can’t be fixed:
php bin/magento module:disable Vendor_Module
php bin/magento setup:upgrade
Or remove via Composer:
composer remove vendor/module-name
Performance Impact Analysis
Quantify the impact before and after fixing non-cacheable blocks:
Check Varnish/FPC Hit Rate
Varnish:
varnishstat -f MAIN.cache_hit -f MAIN.cache_miss
Magento FPC (no Varnish):
# Check X-Magento-Cache-Debug headers
curl -I https://yourstore.com/product-page.html | grep X-Magento-Cache
Expected improvements after fixing:
| Metric | Before (Non-Cacheable) | After (Fixed) | Improvement |
|---|---|---|---|
| TTFB | 2,000-5,000ms | 50-200ms | 90-95% faster |
| Cache Hit Rate | 10-30% | 85-95% | 3-10x more hits |
| Server Load | High CPU, DB queries | Minimal | 10-20x capacity |
| Concurrent Users | 50-100 | 500-1,000 | 10x scalability |
Production Lessons Learned
After optimizing FPC across 50+ Magento stores:
1. Check Third-Party Modules First
95% of non-cacheable block issues come from extensions, not custom code. Always audit new modules before installation:
# Before installing any extension
unzip vendor-extension.zip -d /tmp/extension-review
grep -r 'cacheable.*false' /tmp/extension-review/
If it has non-cacheable blocks, ask the vendor why and if there are alternatives.
2. Widget Blocks Are Often Culprits
CMS widgets inserted via admin panel frequently disable caching. Check:
Admin → Content → Widgets and review each widget’s block class. Search that class for cacheability.
3. Development Mode Disables FPC
Always test cacheability in production mode:
php bin/magento deploy:mode:set production
php bin/magento cache:flush
# Now test your pages
Developer mode bypasses FPC entirely, giving false confidence.
4. Varnish vs Built-In FPC Behave Differently
If using Varnish, non-cacheable blocks still prevent caching at the Magento level, meaning Varnish can’t help. Fix the root cause, don’t rely on external caching layers.
5. Monitor After Every Deployment
Add FPC monitoring to your deployment checklist:
# Post-deployment check
curl -I https://yourstore.com/ | grep X-Magento-Cache-Debug
# Should see: X-Magento-Cache-Debug: HIT (after 2nd request)
If you see MISS repeatedly, investigate immediately.
Related Guides
- Varnish Cache Performance Debugging
- Magento Performance Optimization
- Custom Module Development Best Practices (coming soon)
Tools & Resources
- Mage2TV FPC Extension: Commercial tool for advanced FPC debugging
- Magento DevDocs: FPC Overview
- Community Slack: #performance channel for Magento community