Applying entity and custom cache tags to render arraysfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Picture fifty cached "Recent articles by this author" blocks, one per author on a busy site. Author #23 just edited their bio. Which of those fifty cached blocks needs to be thrown away? Just one — but how would Drupal know which one, out of fifty stored copies, without keeping a manual list somewhere? Cache tags answer exactly this question: label a cached item with what it depends on, and later invalidate everything carrying that label in one call.

What you'll learn in this lesson

  • What a cache tag actually is, and Drupal core's naming convention
  • How to attach tags to a render array so the render cache invalidates correctly
  • Cache::invalidateTags() — the call that clears every tagged item, across every cache bin, in one shot

A cache tag is just a string

There's no special "tag" data type in Drupal — a cache tag is a plain string, conventionally shaped entity_type:id, like node:42 or user:7. Drupal core tags things this way automatically: load a node's render array and it already carries node:42 as a cache tag, with zero code from you. Saving that node later triggers Drupal to call Cache::invalidateTags(['node:42']) internally, which busts every page, block, and view anywhere on the site that was tagged as depending on it — without any of that unrelated code needing to know about each other.

Attaching a custom tag to a render array

$build = [
  '#markup' => $this->t('Recent articles by @name', ['@name' => $author->getDisplayName()]),
  '#cache' => [
    'tags' => ['user:' . $author->id(), 'node_list:article'],
  ],
];

This block is now tagged with both the specific author and the general "any article changed" list tag. Either one being invalidated will correctly expire this cached copy.

Invalidating tags programmatically

use Drupal\Core\Cache\Cache;

Cache::invalidateTags(['user:23']);

Cache::invalidateTags() is a static method that reaches into every registered cache bin at once and marks every entry carrying any of the given tags as invalid — this is the exact mechanism you'll build custom invalidation logic on top of in your own modules, and it's covered in real running code in the Examples module's cache_example, which demonstrates the underlying get()/set() calls this all sits on top of.

Common mistake: reaching for a full cache-clear ("just flush everything") after a data change, out of caution. It works, but it throws away every other cached value on the site along with the one that actually went stale — on a busy site, every visitor pays the cost of a cold cache at once. Tag your cached items properly up front, and a precise invalidateTags() call does the same job with far less collateral damage.

Quick check: your custom block shows the count of unread notifications for the current user. What tag would correctly and efficiently invalidate it the moment a notification is marked read? (Something scoped to that specific user, like user:{$uid} or a custom notifications:{$uid} tag — not a site-wide tag, since that would invalidate the same block for every other user unnecessarily.)

Key takeaways

  • A cache tag is a plain string, conventionally entity_type:id, attached via a render array's #cache['tags'] key.
  • Cache::invalidateTags() is a static, cross-bin operation — the right tool for "invalidate everything depending on X," rather than tracking cache IDs by hand.
  • Reach for the narrowest tag that solves your problem; a full cache flush is a blunt instrument that punishes every visitor, not just the ones affected by the change.

Coming up next

Tags answer "what changed." Next we cover the other half of the puzzle — cache contexts, which answer "does this vary by visitor," so two different users never accidentally see each other's cached content.