Magento 2 Full Page Cache: Custom Tags Guide
What You'll Need
- Magento 2.4.x (examples tested on 2.4.7) - PHP 8.2+ - A working FPC setup (built-in cache, Varnish, or Fastly) - Familiarity with Magento module development (blocks, models, DI) - Basic understanding of HTTP cache headers
---
Key Terms
- FPC (Full Page Cache) — Magento's built-in system for caching complete HTML pages
- IdentityInterface — PHP interface requiring a getIdentities() method; tells Magento which cache tags a block depends on
- Cache tag — a string identifier linking a cached page to a specific entity (e.g., cat_p_42)
- Surgical invalidation — purging only the cached pages affected by a specific change
- X-Magento-Tags header — HTTP response header carrying all cache tags for a given page
- Surrogate keys — Fastly's equivalent of Magento cache tags, sent via the Surrogate-Key header
- VCL — Varnish Configuration Language; controls how Varnish handles requests and purges
---
How Magento 2 Full Page Cache and Cache Tags Work
Full Page Cache (FPC) is one of Magento 2's most powerful performance tools. It's also frequently misunderstood. Most tutorials cover basic setup. The real gains — and the real problems — live deeper.
Before writing any custom code, you need to understand the machinery underneath.
Magento renders a page. The FPC module collects cache tags from every block in that response. Magento stores these tags with the cached HTML. It also sends them to Varnish or Fastly as HTTP response headers.
For built-in entities, this happens automatically. A product page carries the tag cat_p_1 (catalog product ID 1). Magento invalidates that tag whenever product 1 changes. A CMS page tagged cms_p_3 purges when that page updates. When it works, the system is precise and fast.
Here's the flow in plain terms:
IdentityInterfaceX-Magento-Tags response headers\Magento\Framework\App\Cache\TypeListInterface::invalidate() with relevant tagsHere's how the tag header looks on a product page:
X-Magento-Tags: FPC,cat_p,cat_p_42,cat_c,cat_c_5,cms_b_header,cms_b_footer
Varnish indexes pages by these tags. When product 42 saves, only pages carrying cat_p_42 get purged. Not the whole cache. That's surgical invalidation. It breaks entirely when custom blocks don't declare their tags.
---
Identifying Cache Invalidation Problems in Your Store
How do you know you have a cache tag problem? Watch for these signs:
- Stale content persists after saving a record in the admin. Your custom block still shows old data hours later.
- Full cache flushes happen constantly. Your hit rate suffers.
- Cache hit rate below 70% on a store that should be heavily cacheable. Something is over-invalidating.
- Varnish BAN storms visible in varnishstat when a single record saves. One save shouldn't ban hundreds of objects.
The quickest diagnostic is simple. Check whether your custom blocks implement IdentityInterface. Look for getIdentities() in your block class. If it's missing, Magento doesn't know what the block depends on. The result is stale content — or invalidation that's far too broad. This is the problem. Here's how to fix it.
#### Check Which Tags Your Page Is Sending
Inspect the tags for any page with a curl command:
curl -sI https://yourstore.com/your-custom-page | grep -i 'X-Magento-Tags'
If your custom block's tags are missing, they're not being collected. That's your problem.
Warning: Don't test this on a Varnish or Fastly setup that strips
X-Magento-Tagsbefore it reaches you — both typically do for security. Check tags in Magento'sdevelopermode, or temporarily enableX-Magento-Tagspassthrough in your VCL.
You can also enable FPC debug logging. Add this to app/etc/env.php:
'system' => [
'default' => [
'dev' => [
'debug' => [
'debug_logging' => 1
]
]
]
]
Then watch var/log/debug.log to see cache tag collection during page renders.
---
Implementing Custom Cache Tags in Magento 2 Modules
Here's where most custom module development goes wrong. A developer creates a block. The block fetches data from a custom model. It renders output. Nobody tells Magento what the block depends on. Surgical invalidation breaks entirely.
The fix is \Magento\Framework\DataObject\IdentityInterface. Any block rendering entity-specific data must implement this interface.
Setting Up Your Cache Tag Constants
Start in your model. Define a cache tag constant and implement getIdentities():
<?php
declare(strict_types=1);
namespace Vendor\Module\Model;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\DataObject\IdentityInterface;
class Announcement extends AbstractModel implements IdentityInterface
{
public const CACHE_TAG = 'vendor_announcement';
protected $_cacheTag = self::CACHE_TAG;
protected $_eventPrefix = 'vendor_announcement';
public function getIdentities(): array
{
return [self::CACHE_TAG . '_' . $this->getId()];
}
}
Two things matter here. First, the $_cacheTag property on AbstractModel tells Magento's indexer to tag the cache when this model saves. Second, getIdentities() returns the granular tag — vendor_announcement_5 for record ID 5. Both are required.
Implementing IdentityInterface in Your Block
Your block must know which entities it displays. It returns their tags:
<?php
declare(strict_types=1);
namespace Vendor\Module\Block;
use Magento\Framework\View\Element\Template;
use Magento\Framework\DataObject\IdentityInterface;
use Vendor\Module\Model\ResourceModel\Announcement\CollectionFactory;
class AnnouncementList extends Template implements IdentityInterface
{
public function __construct(
Template\Context $context,
private readonly CollectionFactory $collectionFactory,
array $data = []
) {
parent::__construct($context, $data);
}
public function getAnnouncements(): array
{
return $this->collectionFactory->create()
->addFieldToFilter('is_active', 1)
->getItems();
}
public function getIdentities(): array
{
$identities = [\Vendor\Module\Model\Announcement::CACHE_TAG];
foreach ($this->getAnnouncements() as $announcement) {
$identities = array_merge($identities, $announcement->getIdentities());
}
return array_unique($identities);
}
}
This block returns two tag levels. vendor_announcement is the collection-level tag — it fires when any announcement changes. vendor_announcement_3 is the entity-level tag — it fires only for that specific record. Both are necessary.
Pro tip: Always include the collection-level tag alongside entity-level tags. Without it, newly created records won't invalidate the listing page. No existing cached page carries the new record's ID tag yet, so nothing triggers.
Triggering Invalidation on Model Save
AbstractModel handles this automatically when $_cacheTag is set — but only for built-in cache types. For Varnish and Fastly, the PageCache type must also be invalidated.
In most cases, setting $_cacheTag is sufficient. If purges aren't firing, add an explicit plugin on afterSave:
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
use Magento\Framework\App\Cache\TypeListInterface;
use Vendor\Module\Model\Announcement;
class InvalidateAnnouncementCache
{
public function __construct(
private readonly TypeListInterface $cacheTypeList
) {}
public function afterSave(Announcement $subject, Announcement $result): Announcement
{
$this->cacheTypeList->invalidate('full_page');
return $result;
}
}
Register it in di.xml:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Vendor\Module\Model\Announcement">
<plugin name="vendor_module_invalidate_announcement_cache"
type="Vendor\Module\Plugin\InvalidateAnnouncementCache"
sortOrder="10" />
</type>
</config>
---
Integrating Custom Tags with Varnish and Fastly
Getting tags into Magento's collection layer is half the job. Making sure Varnish and Fastly act on them is the other half.
Varnish and the X-Magento-Tags Header
Magento's built-in Varnish VCL handles tag-based BAN purging. When an entity saves, Magento sends a BAN request with a tag regex matching affected pages. Your custom tags flow through automatically — provided your blocks implement IdentityInterface correctly.
Here's the relevant VCL purge logic:
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return (synth(403, "Not allowed."));
}
if (req.http.X-Magento-Tags-Pattern) {
ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
}
return (synth(200, "Purged."));
}
When Magento invalidates vendor_announcement_5, it sends X-Magento-Tags-Pattern: (^|,)vendor_announcement_5(,|$). Varnish bans every cached object whose stored tags match. Clean, targeted, fast.
Fastly Surrogate Keys
Fastly uses surrogate keys — functionally equivalent to Varnish cache tags. Magento sends them via the Surrogate-Key header. The Magento Fastly CDN module (fastly/magento2) translates X-Magento-Tags values into surrogate keys automatically.
Here's what Fastly stores for a page with the custom announcement block:
Surrogate-Key: FPC vendor_announcement vendor_announcement_3 vendor_announcement_7 cat_c_2 cms_b_1
To trigger a purge from a webhook or external system, call the Fastly API directly:
curl -X POST "https://api.fastly.com/service/{SERVICE_ID}/purge/vendor_announcement" \
-H "Fastly-Key: {API_KEY}"
Or use Magento's built-in invalidation from PHP — the Fastly module intercepts it:
<?php
use Magento\PageCache\Model\Cache\Type;
use Magento\Framework\App\Cache\TypeListInterface;
$this->cacheTypeList->invalidate(Type::TYPE_IDENTIFIER);
Pro tip: Confirm
Surrogate-Keyis populated by checking Fastly's real-time logs. If you seeSurrogate-Key: FPCwith no custom tags, yourIdentityInterfaceimplementation isn't being picked up.
One gotcha worth flagging: Fastly has a surrogate key length limit. Category pages with large collections can hit it. Audit your tag output on high-entity-count pages. De-duplicate aggressively.
---
Testing and Validating Your Cache Tag Strategy
Don't ship cache tag changes without validating them. Follow this workflow.
Step 1: Verify Tags Are Being Collected
In developer mode, Magento exposes X-Magento-Tags in responses. Check a page with your custom block:
curl -sI -H "Cookie: PHPSESSID=" https://yourstore.com/announcements | grep X-Magento-Tags
Expected output includes your custom tags:
X-Magento-Tags: FPC,vendor_announcement,vendor_announcement_3,vendor_announcement_7
Step 2: Trigger an Invalidation and Verify the Purge
Save your announcement record in the admin. Then check whether the cached page was purged:
# First request after save — should be a cache MISS
curl -sI https://yourstore.com/announcements | grep -i 'X-Cache'
Second request — should now be a HIT
curl -sI https://yourstore.com/announcements | grep -i 'X-Cache'
For Varnish, look for X-Cache: MISS then X-Cache: HIT. For Fastly, expect X-Cache: MISS, MISS then X-Cache: HIT, HIT — two layers: shield and edge.
Step 3: Confirm Surgical Purging
This is the critical test. Save announcement ID 7. Then verify that pages not carrying vendor_announcement_7 remain cached:
# Check a page that doesn't show announcement 7
curl -sI https://yourstore.com/other-page | grep -i 'X-Cache'
That page should stay a HIT. If it doesn't, your tags are too broad. You're likely returning the collection-level tag where only entity-level tags belong.
Step 4: Load Test the Tag Volume
Under realistic traffic, run a cache warmup script. Trigger saves on several entities simultaneously. Watch Varnish's BAN list:
varnishstat -1 -f MAIN.bans
varnishstat -1 -f MAIN.bans_completed
High bans counts with slow bans_completed rates mean ban evaluation is struggling. This signals over-broad tags generating too many ban expressions.
Warning: Never use
FPCas a standalone cache tag in custom invalidation logic. That tag exists on every cached page. Purging byFPCflushes the entire cache — exactly what you're trying to avoid.
---
Magento 2 custom cache tags aren't complicated once you understand the lifecycle. Implement IdentityInterface on both your model and block. Return granular entity tags alongside collection tags. Let Magento's FPC machinery handle the rest with Varnish or Fastly.
The payoff is real. Cache hit rates stay high even on stores with frequent content updates. Surgical purges complete in milliseconds. Stale content traced to a block that never declared its dependencies disappears entirely.
Get the tags right, and your Adobe Commerce Varnish cache becomes a precision tool instead of a blunt instrument.

