Controlling cache expiration with max-agefor Drupal 8 , 9 , 10 , and 11

Last updated :  

Tags and contexts both answer "should this cached copy still be used?" based on something happening — a save, a different visitor. But some content goes stale purely because time passes: a "weather right now" block, a countdown timer, a feed of "trending this hour" articles. Nothing explicitly invalidates them; they just need to expire. That's max-age.

What you'll learn in this lesson

  • How max-age works as the third piece of Drupal's cache metadata trio
  • The two special values, 0 and Cache::PERMANENT, and what each actually means
  • How max-age interacts with tags and contexts rather than replacing them

Declaring a max-age

use Drupal\Core\Cache\Cache;

$build = [
  '#markup' => $this->buildWeatherSummary(),
  '#cache' => [
    'max-age' => 900, // 15 minutes, in seconds.
  ],
];

After 900 seconds, Drupal treats this cached element as expired and rebuilds it on the next request, regardless of whether any tag was invalidated. Max-age is measured in seconds and is the simplest of the three cache metadata types — no vocabulary to learn, just a number.

The two values worth knowing by heart

  • 'max-age' => 0 — never cache this element at all. Use it for anything that must always be freshly computed: a live view counter, a CSRF-protected form, anything whose staleness would be actively wrong rather than just slightly outdated.
  • Cache::PERMANENT (the implicit default when you omit max-age entirely) — cache forever, until something explicitly invalidates it via a tag. This is the right default for the vast majority of content, since most things only actually change when something tells you they did.
Max-age doesn't replace tags — it complements them. A render array can (and often should) declare both: tags for "invalidate me the instant the underlying data changes" and a modest max-age as a safety net for anything your tagging might have missed. Relying on max-age alone for content that has a clear, taggable dependency is usually the wrong tool — you either wait unnecessarily long for a genuine change to show up, or set the number so low you lose most of the caching benefit.

Quick check: you want a "top 10 most-viewed articles this week" block to refresh a bit more often than once a week, even though nothing about "a view happened" is easily tagged. Would you reach for a cache tag, a cache context, or max-age? (Max-age — this is exactly the "goes stale with time, not with a specific event" case it exists for. A modest max-age, like one hour, balances freshness against not recalculating the ranking on every single request.)

Key takeaways

  • Max-age expires a cached element after a fixed number of seconds, independent of tags or contexts.
  • 0 means never cache; Cache::PERMANENT (the default) means cache until a tag says otherwise.
  • Tags and max-age are complementary, not alternatives — use tags for event-driven invalidation and max-age as a time-based safety net.

Coming up next

You've now met all three pieces of cache metadata. Next, we look at the operational side: how to clear caches efficiently in day-to-day development and production work, without reaching for a full site-wide flush every time.