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

Last updated :  

Picture a whiteboard in a shared office with a sticky note on it that says "Q3 sales numbers." Anyone can glance at the board and instantly know what it's about — and the moment Q3 sales numbers actually change, whoever updates them knows exactly which sticky note to tear off and rewrite. A Drupal cache tag is that sticky note: a plain string label attached to a piece of cached render output, saying "this depends on X" — so that when X changes, Drupal knows precisely which cached copies to throw away, without touching anything else.

What you'll learn in this lesson

  • What a cache tag actually is, and Drupal core's own naming convention for them
  • How to attach a tag to a render array with the #cache property
  • How Cache::invalidateTags() uses tags to clear exactly the right cached items, across every cache bin, in one call
  • Why tagging correctly matters more than caching aggressively

The problem cache tags solve

Say you render a "Recent articles by this author" block and cache it, because building it means querying and rendering several nodes — genuinely expensive work you don't want to repeat on every page view. Now the author edits one of those articles. Somewhere, a cached copy of that block is now stale. Without tags, your only real option is caching for a very short time (defeating the point) or clearing far more than necessary. With tags, you attach node:42 (or whatever that article's node ID is) to the block when you cache it — and the moment node 42 is saved, Drupal's own save logic calls Cache::invalidateTags(['node:42']), which finds and clears every cached item anywhere that was tagged with it. Nothing else is touched.

Attaching a tag to a render array

$build['recent_articles'] = [
  '#theme' => 'item_list',
  '#items' => $article_titles,
  '#cache' => [
    'tags' => ['node:42', 'node_list'],
  ],
];

The #cache property is how any render array declares its own cacheability metadata — tags are just one part of it (you'll meet contexts and max-age, its two siblings, in the next lesson). tags takes a plain array of strings. Here, node:42 says "invalidate me if node 42 changes," and node_list — a generic tag Drupal core itself invalidates whenever any node is saved or deleted — says "invalidate me if the overall set of nodes changes at all," which matters if a *new* article by this author could also affect what "recent" means.

Drupal core's own naming convention

You don't have to invent a tag scheme from scratch — Drupal core already tags every entity as {entity_type_id}:{id} (node:42, taxonomy_term:7, user:3), and every entity's own toUrl()/render output already carries its own tag automatically. When you build something that depends on an entity without being that entity's own render output — like our recent-articles block — you attach that same tag yourself, by hand, exactly as shown above.

Tags compose automatically up the render tree. If your block above sits inside a page render array, Drupal "bubbles" its cache tags up: the whole page's cached copy ends up tagged with node:42 too, without you doing anything extra. This is why a single node edit can correctly invalidate a cached page, a cached block, and a cached view all at once — they were all bubbling the same tag upward.

Invalidating by tag

use Drupal\Core\Cache\Cache;

Cache::invalidateTags(['node:42']);

Cache::invalidateTags() is a static method that reaches into every registered cache bin on the site at once — the render cache, the page cache, dynamic page cache, and any custom bin — and marks every entry carrying that tag as invalid. This is exactly what Drupal core calls, automatically, every time you save a node, taxonomy term, or any other content entity — which is why cached pages usually "just work" correctly without you writing a single line of invalidation code yourself, as long as you tagged things correctly in the first place.

Quick check: you cache a "top 5 comments" block and tag it only with comment_list. A user then edits the wording of one existing comment (not adding or removing any). Will the cached block update? Reasoning it through: comment_list is invalidated when a comment is added or deleted, not necessarily on every edit — so a wording edit on an existing comment might not invalidate it unless you also tagged the block with that specific comment:{id}. This is exactly the kind of gap that tagging too broadly (or too narrowly) creates — always tag for what actually changed, not just what you assume changes.

Key takeaways

  • A cache tag is a plain string describing a dependency; Drupal core's own convention is {entity_type_id}:{id} (e.g. node:42).
  • Attach tags via '#cache' => ['tags' => [...]] on any render array whose output depends on something that can change.
  • Tags bubble automatically up a render tree — a child element's tags become the parent's tags too, all the way to the page cache.
  • Cache::invalidateTags() is a static, cross-bin operation — the same mechanism Drupal core uses internally every time an entity is saved.
  • Generic list tags (node_list, comment_list, etc.) cover "the set changed"; specific entity tags cover "this one thing changed" — use both when a listing depends on both.

Coming up next

Tags answer "when should this be thrown away?" The next lesson tackles a related but different question — "how many different cached copies of this do I even need?" — through cache contexts, using the exact 'user' context Drupal core uses to keep one visitor's cached content from leaking into another's.