Adding Cache Metadata to Drupal Controllers: Tags, Contexts, Max-Agefor Drupal 8 , 9 , 10 , and 11

Last updated :  

Drupal caches rendered output aggressively by default — which is normally a huge performance win, and occasionally exactly the wrong thing. If your controller's output legitimately differs by user, by time, or by some piece of content that might change, you need to tell the cache system about that explicitly. Otherwise the first visitor's version of the page gets frozen and served to everyone else too.

What you'll learn in this lesson

  • The three cache metadata keys: tags, contexts, and max-age
  • How to disable caching entirely for content that must always be fresh
  • Why cache metadata belongs in the render array, not in a separate system

The three pieces of cache metadata

return [
  '#markup' => $this->t('Total published articles: @count', ['@count' => $count]),
  '#cache' => [
    'tags' => ['node_list'],
    'contexts' => ['user.roles'],
    'max-age' => 3600,
  ],
];

tags — invalidate when specific data changes

A cache tag like node_list means: throw away this cached copy the moment anything invalidates that tag — in this case, whenever any node is created, updated, or deleted. Drupal core does this invalidation automatically; you just have to declare which tags your output actually depends on.

contexts — vary the cache by something about the request

A cache context like user.roles tells Drupal to store a separate cached copy for every distinct combination of roles a visitor might have, rather than one copy for everyone. Use the narrowest context that's still correct — user.roles for role-based variation is far cheaper than user, which would cache a separate copy per individual account.

max-age — a hard time limit

max-age is a number of seconds after which the cached copy expires automatically, independent of tags or contexts. Setting it to 0 disables caching for that render element entirely — necessary for genuinely live data, like a session-based counter that must recompute on every single request.

Gotcha: forgetting cache metadata doesn't cause an error — it causes silently stale or silently leaked content. If your output varies by user but you forget a user-related context, one visitor's personalized page can get cached and served to a completely different visitor. This class of bug is easy to miss in testing and painful to track down in production.

Quick check: your controller displays "Welcome back, [username]" for logged-in users. Which cache context do you need, at minimum? 'user' — nothing coarser would work, since the output is genuinely different for every individual account, not just every role.

Key takeaways

  • #cache['tags'] invalidates cached output when specific, named data changes elsewhere on the site.
  • #cache['contexts'] stores separate cached copies varying by something about the request — use the narrowest context that's still accurate.
  • #cache['max-age'] sets a hard expiry in seconds; 0 disables caching for that element entirely.
  • Missing cache metadata doesn't throw an error — it produces silently wrong output, whether that's staleness or, worse, one user's data leaking to another.

Coming up next

Routes and controllers are only useful once real people can reach them safely. The next section moves into access control in depth, starting with Defining Static Permissions — how to declare the permissions your module needs in the first place.