Drupal Cache Tags and Contexts: Invalidate Exactly What Changed
Drupal's Cache API lets you invalidate precisely with cache tags and contexts instead of clearing all caches. Learn how to attach #cache metadata to render arrays, avoid stale content, and keep hit rates high.
23 Dec 2025, 10:53 UTC

The global cache clear trap
You update a node title, but a custom block on the sidebar still shows the old text. The quick fix—running drush cr—works, but it also invalidates every cached page, image style, and route on the site. Under load, that global purge turns a routine content edit into a performance incident.
Drupal's Cache API is designed to avoid this. By attaching cache tags and cache contexts to your render arrays, you tell Drupal exactly what your output depends on and when it should vary. The result is precise invalidation: only the cache entries that actually changed are cleared.
Cache tags: declare dependencies, not just data
A cache tag is a string identifier attached to a cached item that names its dependencies. When the underlying object changes, Drupal invalidates every cache entry carrying that tag. For entities, the tag follows the pattern entity_type:id—for example, node:42.
In custom code, you rarely build these strings by hand. Entity objects provide getCacheTags(), which returns an array including the entity's own tag plus tags for referenced entities and fields. Passing that result into your render array's #cache['tags'] keeps your block in sync with content edits without manual bookkeeping.
Cache contexts: vary output safely
Cache contexts describe request conditions that change the rendered result. If a block displays different links for editors versus anonymous visitors, it needs the user.roles context so Drupal stores a separate cache entry for each role combination. Without it, the first visitor's output could be served to everyone else.
Common contexts include url (the current path), user.roles, and languages (the active language). Choose the narrowest context that still produces correct output. The full user context creates a unique cache entry per user and is almost always too broad for public-facing blocks.
Worked example: a block that stays fresh
Suppose you need a block that displays the title of a specific node—say, a promotional landing page referenced by ID. The following block plugin lives in modules/custom/my_module/src/Plugin/Block/NodeTitleBlock.php.
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\node\Entity\Node;
/**
* @Block(
* id = "node_title_block",
* admin_label = @Translation("Node title"),
* )
*/
class NodeTitleBlock extends BlockBase {
public function build() {
// Replace with the node ID you want to display.
$nid = 42;
$node = Node::load($nid);
if (!$node) {
return [
'#markup' => $this->t('Node not found.'),
// No cache tags: the output is static.
'#cache' => ['max-age' => 0],
];
}
return [
'#markup' => $node->getTitle(),
'#cache' => [
'tags' => $node->getCacheTags(),
'contexts' => ['url'],
],
];
}
}
After enabling the module and placing the block, edit the node title through the content UI. The block updates on the next page load without a global cache clear. If it does not refresh, confirm that the block is not being overridden by a contrib module that strips render cache metadata.
Where this fits with reverse proxies
The #cache keys are not limited to Drupal's internal cache bins. When a page is assembled, Drupal aggregates the metadata from every render array and exposes it to the response. Reverse proxies such as Varnish or a CDN can consume those tags to purge their own edge caches, extending the same precision beyond the application server.
The trade-off: fragmentation versus staleness
Cache metadata is a balance. Attach too many contexts and you fragment the cache: every role, URL, or language combination becomes a separate entry, and your hit rate collapses. Attach too few tags and you serve stale content after an update.
A practical rule is to start with entity tags and the url context, then add contexts only when you observe incorrect output leaking between pages or users. Profiling tools such as Webprofiler or New Relic can show cache hit ratios before and after you refine metadata, turning guesswork into measurement.
Limits and verification
Cache contexts are not a security boundary. A context controls which cached variant is served; it does not enforce access. If a block contains data that only certain roles may see, enforce permissions in the block's access() method or route definition in addition to setting user.roles.
The Cache API has been stable since Drupal 8, but internal cache bins and contrib integration patterns can differ between major versions. Verify module-specific invalidation behavior in your target Drupal version before relying on it in production.
To verify your setup, update the referenced node and reload the page as an anonymous user. The title should change immediately. Then log in as a user with a different role and confirm the block still renders correctly. If either check fails, inspect the render array for missing #cache keys or conflicting contrib overrides.
Actionable next step
Audit one custom block or controller this week. Add $entity->getCacheTags() to every render array that depends on an entity, and add the narrowest context that matches your personalization logic. Measure the cache hit rate before and after. You will likely find that most of your global cache clears were compensating for metadata you never declared.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.