Decides which cache tag invalidations reach which caches, and which tags CDNs see. Every entity save invalidates cache tags, and Drupal passes them to its own caches and to every CDN purger. Many of those invalidations purge nothing useful: a contact form message on a site that never shows messages publicly, or a visitor's comment emptying every node listing. Cache Purifier lets you drop them with rules, per target, by tag, role, path and origin.
- CDN purge limits. Purges for tags no public page carries still count against the CDN's rate limits.
- Purge storms. On a busy site, list tags such as
node_listare invalidated constantly, and every invalidation empties every listing. - Header limits. CDNs cap their tag headers (bunny.net at 1024 bytes), and a listing page can carry hundreds of tags. The outbound policy folds and orders them so the useful ones fit.
- One place for it. CDN modules each grow their own blocklist. Rules apply to any purger that registers as a cache tags invalidator: a CDN module such as bunny.net, a Varnish integration, the Purge module's queuer, or your own.
- Drupal 11.3 or later, or Drupal 12
- PHP 8.3 or later
composer require drupal/cache_purifier
drush en cache_purifier
The module replaces the class of the cache_tags.invalidator service. If
another module replaces that service too, the status report says the rules are
not applied.
Everything is in cache_purifier.settings, edited under Configuration →
Development → Cache Purifier (/admin/config/development/cache-purifier) or
in the exported config file.
- Rules lists the rules with what each drops, from where and when. Each rule has its own form: the tags, the targets as checkboxes of the invalidators on the site, and the conditions under Only when. The form saves the rule in the shape shown below.
- Outbound tags holds the blocklist, aliases, buckets and the tag report.
rules:
private_entities:
label: 'Entities no public page shows'
tags: ['contact_message:*', 'shortcut:*', 'workspace:*']
targets: [bunny_net.cache_tags_invalidator]A rule drops the tags it matches for its targets. A tag reaches a target unless any enabled rule drops it.
| Key | Default | What it does |
|---|---|---|
label |
Shown in the status report. | |
status |
true |
Turns the rule off when false. |
tags |
none | Tags to drop. Patterns match the whole tag and * matches any characters, so *_list drops node_list but leaves node_list:article alone. * drops every tag. |
except |
none | Tags to keep even when tags matches them. |
targets |
none | Where to drop them: local, an invalidator's service ID, or all. |
roles |
any user | The rule holds when the current user has no role outside this list. Include anonymous or authenticated as needed. |
paths |
any path | The rule holds when the request that invalidates matches one of these paths, one per line, such as /user/*. The path alias is checked too. |
origins |
any | web, cli (Drush and command-line cron), or both. |
unless_header |
The rule does not hold when the request has this header, for example one an API client sends. |
The conditions combine with AND. A rule without tags or targets does
nothing.
localis Drupal's own caches: the cache bins and the cache tag checksum, whether core's database one or Redis's. Dropping a tag there leaves stale data in Drupal itself, so the status report warns about every rule that does.- An invalidator's service ID targets that one, such as
bunny_net.cache_tags_invalidator. The status report lists the IDs on your site. allis every invalidator andlocal.
Visitors' actions don't purge the CDN, except through a decoupled front end's API:
visitors:
tags: ['*']
targets: [bunny_net.cache_tags_invalidator]
roles: [anonymous, authenticated]
unless_header: X-Api-ClientUsers without an editorial role don't invalidate list tags anywhere, apart from comment lists:
users_keep_lists:
tags: ['*_list', '*_list:*']
except: [comment_list]
targets: [all]
roles: [anonymous, authenticated]Nothing done on user account pages purges the CDN:
account_pages:
tags: ['*']
targets: [bunny_net.cache_tags_invalidator]
paths: "/user\n/user/*"The Cache Purifier examples submodule adds a few example rules, disabled. See its README.
Outbound tags are the tags sent outside Drupal: the response header a CDN or a
reverse proxy such as Varnish caches pages by, and the purges sent to it.
Modules that use Cache Purifier for this, such as bunny.net, write
OutboundTags::forHeader() into the header and purge with
OutboundTags::forPurge(). Both map every tag the same way, so a purge always
reaches the pages that carried the tag. Any integration can do the same, for
example a Varnish setup that bans by a tag header. Drupal's own caches are not
affected.
outbound:
blocklist: [http_response, rendered, library_info, 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:*']
aliases:
- { from: 'node_stats:*', to: 'node:*' }
buckets:
- { tags: 'taxonomy_term:*', count: 16 }-
Blocklist: tags never written or purged.
renderedis here because every theme or config change invalidates it, which would empty the CDN. -
Aliases rewrite a tag before it is written or purged. Each
*infromcaptures what it matches and fills the matching*into.- Merge. A custom
node_statsentity is shown on the page of the node with the same ID. Withnode_stats:* | node:*,node_stats:5becomesnode:5: a page showing both carries one tag instead of two, and saving node_stats 5 purges the pages of node 5. - Rename.
taxonomy_term:* | term:*writes and purgesterm:5, shorter where tags are sent as plain text. Formats that hash every tag to the same length, such as bunny.net's, gain nothing from it.
Drupal's own caches keep the original names. To rename tags there too, override
getCacheTagsToInvalidate()in a bundle class. - Merge. A custom
-
Buckets fold a family of tags into a fixed number of tags. With
taxonomy_term:*in 16 buckets, every term tag becomes one oftaxonomy_term:~0totaxonomy_term:~15, picked by a hash of the tag. A page showing 200 terms carries at most 16 term tags, leaving room in the header for the rest. The cost: saving a term purges every page showing any term in its bucket, about one in 16 of them. Suits tags that are many on a page and rarely change. Start the pattern with a literal prefix, which names the buckets.
Headers list tags by importance, so a proxy that cuts a full header drops the
least useful ones: list tags (node_list, node_list:article), entity
tags (node:12, buckets), config tags, then the rest.
The blocklist is for tags no CDN or proxy should ever see. To stop purges only for some users, paths or purgers, use a rule.
The report shows which kinds of tags are invalidated, and which kinds ever
appear in an outbound header. A kind is a tag with its ID replaced by *, such as
contact_message:*. A kind that is invalidated often but never appears on a
public page only costs purges.
It is off by default. Turn it on in the form or with
drush config:set cache_purifier.settings report.enabled 1, let real traffic
run, then:
$ drush cache-purifier:report
Kind Invalidations Last invalidated First in a header
contact_message:* 9830 2026-10-27 09:12
node:* 1204 2026-10-27 09:10 2026-10-26 23:11
Never in a header: contact_message:*
$ drush cache-purifier:report --apply=bunny_net.cache_tags_invalidator
--apply adds the suggestions to a report_suggestions rule for that target,
after asking. Export config to keep it. --reset forgets what was collected.
Counts from concurrent requests can overwrite each other, so they are
approximate.
| Command | Alias | What it does |
|---|---|---|
cache-purifier:report |
cp-report |
Show the tag report. --apply=<target>, --reset. |
- Valentino Međimorec - valic