Cache Contexts: Varying Cached Output by User in Drupalfor Drupal 11 , and 10

Last updated :  

You've learned what to cache and when to throw it away. This final lesson in the Cache API topic covers a subtler, easy-to-miss question: who should see which cached copy? Get this wrong and you get a genuinely embarrassing class of bug — one visitor's personalized greeting, cached and then served to a completely different visitor. The Drupal concept that prevents this is called a cache context.

Heads up before we start: this lesson's code comes from a different Examples module than the previous two — render_example, not cache_example — because its arrays() controller happens to contain the clearest real-world example of a cache context in the whole Examples project. On this install, the specific /examples/render-example/arrays page that method builds has a genuine upstream bug: it tries to debug-print render array values that hold live references back into Drupal's service container, and that circular reference sends PHP's var_export() into a loop that never finishes. It's a real bug in the module, not something wrong with your setup — and it's worth knowing this kind of thing happens even in official, well-maintained code. The screenshot below instead shows the module's working "Alter pages and blocks" page, and the code walkthrough is unaffected — the class and method shown are exactly what's installed and what the concepts below describe.

What you'll learn in this lesson

  • What a cache context is, and why it's a different concept from a cache tag
  • Why the 'user' context exists and what problem it prevents
  • How cache contexts form a hierarchy, and why picking the least specific one matters
  • How high-cardinality contexts connect to #lazy_builder and placeholders

The problem cache contexts solve

Imagine a block of markup that says "Hello, Sarah, welcome back." If Drupal's renderer caches that block the same way it would cache any other content, the very next visitor — someone named James — could be served Sarah's cached greeting. That's not a hypothetical: it's exactly what happens if you cache per-user content without telling Drupal that the content varies by user. A cache context is how you tell it.

The source file

Path (relative to the Examples module's root): modules/render_example/src/Controller/RenderExampleController.php

<?php

namespace Drupal\render_example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\Markup;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\examples\Utility\DescriptionTemplateTrait;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Utility\Variable;
use Drupal\Core\Session\AccountInterface;

/**
 * Provides module description page and examples of building render arrays.
 *
 * Controllers that respond to a route should always return their content as
 * a renderable array. See the arrays() method below as an example.
 *
 * @ingroup render_example
 */
class RenderExampleController extends ControllerBase implements TrustedCallbackInterface {

  use DescriptionTemplateTrait;

  /**
   * Constructs a new BlockController instance.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(AccountInterface $current_user) {
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}
   */
  protected function getModuleName() {
    return 'render_example';
  }

  /**
   * Examples of defining content using renderable arrays.
   *
   * Methods on a controller that are the target of a route should return a
   * renderable array which contains any content to display for that route.
   */
  public function arrays() {
    // The core structure of the Render API is the render array, which is a
    // hierarchical associative array containing data to be rendered and
    // properties describing how the data should be rendered. Whenever a module
    // needs to output content it should do so be defining that content as a
    // renderable array. Below we'll look at some common examples of how render
    // arrays can be used to define content.
    $build = [];

    // CSS and JavaScript libraries can be attached to elements in a renderable
    // array. This way, if the element ends up being rendered and displayed you
    // know for sure the CSS/JavaScript will also be included. But, if for
    // some reason the element isn't ever rendered then Drupal can skip the
    // unnecessary extra files.
    //
    // Learn more about attaching CSS and JavaScript libraries with the
    // #attached property here:
    // https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/#sec_attached
    $build['#attached'] = [
      'library' => [
        'render_example/render-example.library',
      ],
    ];

    // Renderable arrays have two kinds of key/value pairs: properties and
    // children. Properties have keys starting with '#' and their values
    // influence how the array will be translated to a string. Children are all
    // elements whose keys do not start with a '#'. Their values should be
    // renderable arrays themselves.
    //
    // This example defines a new element, 'simple', that contains two
    // properties; '#markup' and '#description'. This is the quickest way to
    // output a string of HTML.
    $build['simple'] = [
      '#markup' => '<p>' . $this->t('This page contains examples of various content elements described using render arrays. Read the code and comments in \Drupal\render_example\Controller\RenderExampleController::arrays() for more information.') . '</p>',
      '#description' => $this->t('Example of using #markup'),
    ];

    // Additional properties can be used to further define the content. In this
    // case '#prefix' and '#suffix' are being used to provide strings to add
    // before, and after, the main content. This is useful because now the tag
    // being used to wrap the block of content can be easily changed without
    // having to worry about the content. Or, the tag can easily be left out
    // during rendering if for example the content is being output as JSON.
    //
    // There is a set of common properties that can be used for all elements in
    // a render array. These are defined by
    // \Drupal\Core\Render\Element\RenderElementBase. Most elements also have
    // additional element type specific properties.
    //
    // Figuring out what additional properties are available requires first
    // determining what sort of render element you're dealing with. Look for the
    // presence of one of these properties to start:
    // - #markup, or #plain_text: These are the simplest render arrays, and are
    //   used to display simple strings of text. In addition to the common set
    //   of properties available for all elements #markup elements can use the
    //   #allowed_tags property, an array of additional tags to allow when the
    //   HTML string is run through \Drupal\Component\Utility\Xss::filterAdmin()
    //   to strip out possible XSS vectors.
    // - #theme: The presence of #theme indicates that the array contains data
    //   to be themed by a particular theme hook. The available properties will
    //   depend on the specific theme hook. See the example below for more about
    //   determining what properties to use.
    // - #type: The presence of #type indicates that the array contains data and
    //   options for a particular type of "render element" (for example, 'form',
    //   'textfield', 'submit', for HTML form element types; 'table', for a
    //   table with rows, columns, and headers). The additional properties will
    //   depend on the render element type, and are documented on the class that
    //   defines the element type.
    //
    $build['simple_extras'] = [
      '#description' => $this->t('Example of using #prefix and #suffix'),
      // Note the addition of '#type' => 'markup' in this example compared to
      // the one above. Because #markup is such a commonly used element type you
      // can exclude the '#type' => 'markup' line and it will be assumed
      // automatically if the '#markup' property is present.
      '#type' => 'markup',
      '#markup' => '<p>' . $this->t('This one adds a prefix and suffix, which put a blockquote tag around the item.') . '</p>',
      '#prefix' => '<blockquote>',
      '#suffix' => '</blockquote>',
    ];

    // In addition to #markup, you can also use #plain_text to output, you
    // guessed it, strings of plain text. This indicates that the array contains
    // text which should be escaped before it is displayed.
    $build['simple_text'] = [
      '#plain_text' => '<em>This is escaped</em>',
      '#description' => $this->t('Example of using #plain_text'),
    ];

    // Using the '#theme' property for an element specifies that the array
    // contains data to be themed by a particular theme hook. Essentially using
    // a Twig template to generate the HTML for an element. Modules define theme
    // hooks by implementing hook_theme(), which specifies the input "variables"
    // used to provide data and options; if a hook_theme() implementation
    // specifies variable 'separator', then in a render array, you would provide
    // this data using the '#separator' property.
    //
    // @see hook_theme()
    $build['theme_element'] = [
      // The '#theme' property can be set to any valid theme hook. For more
      // information about theme hooks, and to discover available theme hooks
      // that you can use when creating render arrays see the documentation for
      // hook_theme().
      //
      // Many of the most commonly used theme hooks are defined in
      // drupal_common_theme().
      '#theme' => 'item_list',
      '#title' => $this->t('Example of using #theme'),
      // The #items property is specific to the 'item_list' theme hook, and
      // corresponds to the variable {{ items }} in the item-list.twig.html
      // template file.
      '#items' => [
        $this->t('This is an item in the list'),
        $this->t('This is some more text that we need in the list'),
      ],
    ];

    // Using the '#type' property for an element specifies that the array
    // contains data and options for a particular type of "render element".
    // Render element types can be thought of as prepackaged render arrays that
    // provide default values for a set of properties as well as code that
    // will perform additional processing on the array before it is rendered.
    //
    // As an example take a look at the code in
    // \Drupal\Core\Render\Element\Table::getInfo(). Notice that it is defining
    // values for #theme, and #process? These values will be merged with
    // whatever properties you define in your code.
    //
    // In addition, most render element types have type specific properties. A
    // table for example has #header, and #rows properties. The easiest way to
    // determine what element type specific properties exist is to read the
    // documentation for the class that defines the element type. Don't forget
    // that it will also inherit properties used by any class it is extending.
    //
    // There are two types of render element types:
    // - Generic elements: Generic render element types encapsulate logic for
    //   generating HTML and attaching relevant CSS and JavaScript to the page.
    //   These include things like link, table, and drop button elements.
    // - Form elements: Most of the render element types provided by core
    //   represent the various widgets you might use on a form. Text fields,
    //   password fields, and file upload buttons for example. These elements
    //   are intended to be used in conjunction with a form controller class
    //   and have additional properties such as `#required`, and
    //   `#element_validate`, related to their use as part of a form. For more
    //   on form elements check out the fapi_example module.
    $build['table'] = [
      // The value used for #type is the ID of the plugin that implements the
      // element type you want to use. This can be inferred from the annotation
      // for the element.
      // You can also find a list of element types provided by Drupal core here
      // https://api.drupal.org/api/drupal/elements.
      '#type' => 'table',
      '#caption' => $this->t('Our favorite colors.'),
      '#header' => [$this->t('Name'), $this->t('Favorite color')],
      '#rows' => [
        [$this->t('Teal'), $this->t('teal')],
        [$this->t('Amber'), $this->t('green')],
        [$this->t('Blake'), $this->t('#063')],
        [$this->t('Enid'), $this->t('indigo')],
        [$this->t('Joe'), $this->t('green')],
      ],
      '#description' => $this->t('Example of using #type.'),
    ];

    // Render arrays can be nested any level deep. This allows you to group
    // like things together. A great example of this is the $page array used in
    // conjunction with the page.html.twig template. The top level contains all
    // the regions, each of which contain the blocks placed in that region,
    // which in turn contain their own content. In fact, when this array is
    // ultimately displayed on a page it will be as part of the $page array.
    $build['nested_example'] = [
      '#description' => $this->t('Example of nesting elements'),
      '#markup' => '<p>' . $this->t('Render arrays can contain any number of nested elements. During rendering, the innermost elements are rendered first, and their output is incorporated into the parent element.') . '</p>',
      'nested_child_element' => [
        // An un-ordered list of links.
        // See /core/modules/system/templates/item-list.html.twig.
        '#theme' => 'item_list',
        '#title' => $this->t('Links'),
        '#list_type' => 'ol',
        '#items' => [
          Link::fromTextAndUrl($this->t('Drupal'), Url::fromUri('https://www.drupal.org')),
          Link::fromTextAndUrl($this->t('Not Drupal'), Url::fromUri('https://wordpress.org/')),
        ],
      ],
    ];

    // Example of adding a link using the #link element type.
    $build['nested_example']['another_nested_child'] = [
      // See \Drupal\Core\Render\Element\Link.
      '#type' => 'link',
      '#title' => $this->t('A link to example.com'),
      '#url' => Url::fromUri('https://example.com'),
    ];

    // The #theme_wrappers property can be used to provide an array of theme
    // hooks which provide the envelope or "wrapper" of a set of child elements.
    // The theme function finds its element children (the sub-arrays) already
    // rendered in '#children'.
    $build['theme_wrappers demonstration'] = [
      '#description' => $this->t('Example of using #theme_wrappers'),
      'child1' => ['#markup' => $this->t('Markup for child1')],
      'child2' => ['#markup' => $this->t('Markup for child2')],
      '#theme_wrappers' => [
        'render_example_add_div',
      ],
    ];

    // Use the #access property to control who can see what content. If an
    // element in an render array has its #access property set to FALSE it will
    // be removed from the array before rendering. And thus not visible.
    $build['access_example'] = [
      '#description' => $this->t('Example of using #access to control visibility'),
      '#markup' => $this->t('This text is only visible to authenticated users.'),
      '#access' => $this->currentUser->isAuthenticated(),
    ];

    // Some properties define callbacks, which are callable functions or methods
    // that are triggered at specific points during the rendering pipeline.
    $build['pre_render_and_post_render'] = [
      '#description' => $this->t('Example of using #pre_render and #post_render'),
      '#markup' => '<div style="color:green">' . $this->t('markup for pre_render and post_render example') . '</div>',
      // #pre_render callbacks are triggered early in the rendering process,
      // they get access to the element in the array where the callback is
      // named, and all of its children. They can be used to do things like
      // conditionally alter the value of a property prior to the array being
      // rendered to HTML.
      '#pre_render' => [static::class . '::preRenderAddSuffix'],
      // #post_render callbacks are triggered after the array has been rendered
      // and can operate on the rendered HTML. They also have access to the
      // original array for context.
      '#post_render' => [static::class . '::postRenderAddPrefix'],
    ];

    // Properties that contain callbacks can also reference methods on a class
    // in addition to functions. See
    // \Drupal\render_example\Controller\RenderExampleController::preRender()
    // @todo This doesn't work, we need to fix it.
    // https://www.drupal.org/project/examples/issues/2986435
    // $build['#pre_render'] = [static::class, 'preRender'];.
    // Caching is an important part of the Render API, converting an array to a
    // string of HTML can be an expensive process, and therefore whenever
    // possible the Render API will cache the results of rendering an array in
    // order to improve performance.
    //
    // When defining a render array you should use the #cache property to make
    // an element cacheable.
    $build['cache_demonstration'] = [
      '#description' => $this->t('#cache demonstration'),
      // This string contains information that is specific to the user who is
      // currently viewing the page. We can cache it, and re-use the string any
      // time the same user views the page again. However, if the user changes,
      // or if the user changes their name, we need to expire the cached data
      // and rebuild it so that it is accurate.
      '#markup' => $this->t('Hello @name, welcome to the #cache example.', ['@name' => $this->currentUser->getAccountName()]),
      // The #cache property is used to provide metadata about the element being
      // cached, and the conditions under which it should be expired. This can
      // be time based, or context based. You can read more about caching
      // render arrays here
      // https://www.drupal.org/docs/8/api/render-api/cacheability-of-render-arrays
      '#cache' => [
        // The "current user" is used above, which depends on the request, so
        // we tell Drupal to vary by the 'user' cache context.
        'contexts' => [
          'user',
        ],
      ],
    ];

    // A #lazy_builder callback can be used to build a highly dynamic section of
    // a render array from scratch. This, combined with the use of placeholders,
    // allows the renderer to cache some, but not all, portions of a render
    // array. Without #lazy_builders, if any element in the render tree is
    // uncacheable the whole tree would need to be re-rendered every time.
    //
    // The general rendering flow is as follows:
    // - Check for cached version of output from previous rendering, if it
    //   exists replace any placeholders in the rendered output with their
    //   dynamic content as generated by the #lazy_builder callback, and return
    //   the resulting HTML.
    // - If no cached version exists render the array to HTML, when an element
    //   that can be placeholdered is encountered insert a placeholder, cache
    //   the HTML after rendering for next time, replace the placeholders with
    //   their dynamic content, and return the resulting HTML.
    //
    // This is especially noticeable when used in conjunction with modules like
    // Big Pipe which do rendering of a page in multiple passes vs. the default
    // single flush renderer.
    //
    // See \Drupal\block\BlockViewBuilder::viewMultiple() for an example from
    // core.
    $build['lazy_builder'] = [
      // Set the value of the #lazy_builder property to an array, the first key
      // of the array is the method, service, or function, to call in oder to
      // generate the dynamic data. The second argument is an array of any
      // arguments to pass to the callback. Arguments can be only primitive
      // types (string, bool, int, float, NULL).
      '#lazy_builder' => [
        static::class . '::lazyBuilder',
        [$this->currentUser->id(), 'Y-m-d'],
      ],
      // #lazy_builder callbacks can be used in conjunction with
      // #create_placeholder to tell the renderer that instead of simply calling
      // the #lazy_builder code right away, to instead insert a placeholder and
      // delay execution of the #lazy_builder code until it's needed.
      //
      // This is somewhat analogous to the way Drupal uses the PSR-4 autoloading
      // standard to "lazy" load PHP files that contain the definition of a
      // class only if, and when, that class is used.
      //
      // To force a element to use a placeholder set #create_placeholder to
      // TRUE.
      //
      // Alternatively you could include #cache metadata (see above) and allow
      // the Render API to use that metadata to automatically determine based on
      // the existence of high-cardinality cache contexts in the subtree whether
      // or not the element should use a placeholder.
      '#create_placeholder' => TRUE,
    ];

    // Example of the marquee element type defined by
    // \Drupal\render_example\Element\Marquee.
    $build['marquee'] = [
      '#description' => $this->t('Example custom element type'),
      '#type' => 'markup',
      'marquee_element' => [
        '#type' => 'marquee',
        '#content' => $this->t('Hello world!'),
      ],
    ];

    $output = [];
    // We are going to create a new output render array that pairs each
    // example with a set of helper render arrays.
    foreach (Element::children($build) as $key) {
      if (isset($build[$key])) {
        $output[$key] = [
          '#theme' => 'render_array',
          'description' => [
            '#type' => 'markup',
            '#markup' => $build[$key]['#description'] ?? '',
          ],
          'rendered' => $build[$key],
          'debug_value' => [
            '#type' => 'markup',
            '#markup' => htmlentities(Variable::export($build[$key])),
          ],
        ];
      }
    }

    foreach (Element::properties($build) as $key) {
      $output[$key] = $build[$key];
    }

    return $output;
  }

  /**
   * A #pre_render callback, expand array to include additional example info.
   *
   * This method is called during the process of rendering the array generated
   * by \Drupal\render_example\Controller\RenderExampleController::arrays().
   *
   * This also demonstrates how a #pre_render callback could be used to expand
   * a relatively simple array into multiple individual renderable elements
   * based on application logic.
   *
   * @param array $element
   *   Pre render methods (and functions) get a single argument that is the
   *   render API array representing the element where the #pre_render property
   *   was defined, and all of it's children.
   *
   * @return array
   *   Pre render methods (and functions) should return the modified render
   *   array.
   */
  public static function preRender(array $element) {
    // For each first level child element lets add some additional helpful
    // output. \Drupal\Core\Render\Element::children() is a utility method that
    // allows you to quickly identify all children of a render array. That is
    // those key/value pairs whose key does not start with a '#'.
    foreach (Element::children($element) as $key) {
      $child = $element[$key];
      unset($element[$key]);
      if (isset($child['#description'])) {
        $element[$key] = [
          // The value from the #description property will be used as a title
          // for this element in the final output.
          'description' => [
            '#markup' => $child['#description'],
          ],
          // Move the original element to 'rendered'. The rendering process is
          // recursive so this will still be located, and rendered to HTML.
          'rendered' => $child,
          // Export the element definition as a string of text so we can display
          // the array that was used to create the rendered output just below
          // the output.
          'debug_value' => [
            '#markup' => htmlentities(Variable::export($child)),
          ],
          '#theme' => 'render_array',
        ];
      }
    }

    // Return our modified version of the original $element.
    return $element;
  }

  /**
   * Example #lazy_builder callback.
   *
   * Demonstrates the use of a #lazy_builder callback to build out a render
   * array that can be substituted into the parent array wherever the cacheable
   * placeholder exists.
   *
   * This method is called during the process of rendering the array generated
   * by \Drupal\render_example\Controller\RenderExampleController::arrays().
   *
   * @param string $date_format
   *   Date format to use with \Drupal\Core\Datetime\DateFormatter::format().
   *
   * @return array
   *   A renderable array with content to replace the #lazy_builder placeholder.
   */
  public static function lazyBuilder($date_format) {
    $build = [
      'lazy_builder_time' => [
        '#markup' => '<p>' . \Drupal::translation()->translate('The current time is @time', [
          '@time' => \Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime(), 'long'),
        ]) . '</p>',
      ],
    ];

    return $build;
  }

  /**
   * Example '#post_render' callback function.
   *
   * Post render callbacks are triggered after an element has been rendered to
   * HTML and can act upon the final rendered string.
   *
   * This function is used as a post render callback in
   * Drupal\render_example\Controller\RenderExampleController::arrays().
   *
   * @param string $markup
   *   The rendered element.
   * @param array $element
   *   The element which was rendered (for reference)
   *
   * @return string
   *   Markup altered as necessary. In this case we add a little postscript.
   *
   * @see \Drupal\render_example\Controller\RenderExampleController::arrays()
   */
  public static function postRenderAddPrefix($markup, array $element) {
    $markup .= '<div style="color:blue">This markup was added after rendering by a #post_render callback.</div>';
    return $markup;
  }

  /**
   * Example '#pre_render' function.
   *
   * Pre render callbacks are triggered prior to rendering an element to HTML
   * and are given the chance to manipulate the renderable array. Any changes
   * they make will be reflected in the final rendered HTML.
   *
   * We need to wrap suffix in a Markup object.
   * Otherwise, style attribute will be removed by Xss
   *
   * @see \Drupal\Component\Utility\Xss::filter()
   *
   * This function is used as a post render callback in
   * \Drupal\render_example\Controller\RenderExampleController::arrays().
   *
   * @param array $element
   *   The element which will be rendered.
   *
   * @return array
   *   The altered element. In this case we add a #prefix to it.
   *
   * @see \Drupal\render_example\Controller\RenderExampleController::arrays()
   */
  public static function preRenderAddSuffix(array $element) {
    $element['#suffix'] = Markup::create('<div style="color:red">'
      . t('This #suffix was added by a #pre_render callback.') . '</div>');
    return $element;
  }

  /**
   * {@inheritDoc}
   */
  public static function trustedCallbacks() {
    return ['postRenderAddPrefix', 'preRenderAddSuffix', 'lazyBuilder'];
  }

}

That's the whole file — deliberately reproduced in full even though this lesson only discusses two of its elements, because that's the standard this course holds every lesson to. If you've already read the earlier Render API lessons you'll recognize most of it; the two elements that matter for cache contexts specifically — cache_demonstration and lazy_builder — are called out below.

How it works

The #cache property and the contexts key

$build['cache_demonstration'] = [
  '#markup' => $this->t('Hello @name, welcome to the #cache example.', [
    '@name' => $this->currentUser->getAccountName(),
  ]),
  '#cache' => [
    'contexts' => [
      'user',
    ],
  ],
];

This is the core example. The #cache property is where any render array element declares its caching metadata. It has three possible sub-keys — contexts (this lesson's focus), tags (the previous lesson's focus), and max-age (a time-to-live in seconds). contexts answers a completely different question from tags: tags describe what data the content depends on; contexts describe who or what request condition the content varies by.

Why 'user', specifically?

The markup embeds $this->currentUser->getAccountName() — genuinely different text for every logged-in visitor. Declaring the 'user' context tells Drupal's render cache: "store a separate cached copy of this element for every individual user account." Without it, the first person to load the page bakes their name into a cached copy that every subsequent visitor would then see. Common contexts you'll meet in real code:

ContextVaries by
userThe exact logged-in user account
user.permissionsThe set of permissions the user holds
user.rolesThe roles assigned to the user
urlThe full current URL
url.pathOnly the path portion of the URL
url.query_argsThe query string parameters
languagesThe current interface language
themeThe active theme

The hierarchy — and why "least specific" wins

Cache contexts are hierarchical: user is more specific than user.permissions, which is more specific than user.roles. "More specific" directly translates to "more separate cached copies stored" — user can produce one cached copy per individual person on the site, while user.roles only produces one copy per unique combination of roles (often just a handful, site-wide). The rule of thumb: always declare the least specific context that still correctly captures how your content actually varies. If your markup only changes based on whether someone is an administrator, use user.roles — not the far more expensive user.

High-cardinality contexts and #lazy_builder

A context like 'user' is called high-cardinality — it can multiply into one cached copy per visitor on a busy site, which quickly becomes an inefficient way to cache a full page. Drupal's render pipeline handles this automatically: elements with high-cardinality contexts get promoted to use a #lazy_builder placeholder, so the rest of the page can still be cached once and shared across everyone, while only the small per-user fragment gets rebuilt separately on each request:

$build['lazy_builder'] = [
  '#lazy_builder' => [
    static::class . '::lazyBuilder',
    [$this->currentUser->id(), 'Y-m-d'],
  ],
  '#create_placeholder' => TRUE,
];

The first item in the #lazy_builder array is the callable to run; the second is an array of arguments, which must be primitive values only (strings, numbers, booleans) since they need to be safely reproducible from a placeholder. #create_placeholder => TRUE forces this behavior explicitly rather than waiting for Drupal to infer it from the cache metadata.

Common beginner mistake: confusing cache tags with cache contexts because they both live under #cache. A quick way to keep them straight: contexts answer "who is asking?" (varies the cache key up front), tags answer "what changed?" (invalidates existing cached copies after the fact). You'll often use both together on the same element.

See it for yourself

Visit /examples/render-example/altering on your DDEV site — the render_example module's working demo page, showing render arrays and theming in action (see the note at the top of this lesson for why we're using this page rather than the buggy /arrays sub-page).

The render_example module's working demo page showing render arrays and theming

Quick check: a block shows different content depending only on whether the visitor is logged in or anonymous — never anything more specific than that. Would 'user' or 'user.roles' (or even simpler, a context tied to authentication status) be the more efficient choice? The less specific option always wins when it still correctly captures the variation — reach for the narrowest context, not the safest-sounding one.

Key takeaways

  • The #cache property's contexts sub-key declares who a cached element varies by, preventing one visitor's cached output from leaking to another.
  • Use 'user' only when content is genuinely tied to one specific account; prefer the less specific 'user.permissions' or 'user.roles' whenever the variation is really about what someone can do or which group they're in.
  • Cache contexts are hierarchical — always declare the least specific one that correctly represents your content's variation, to keep the number of cached copies low.
  • High-cardinality contexts like 'user' automatically push Drupal toward #lazy_builder placeholders, so the rest of a page stays cacheable while only the per-user fragment is rebuilt on each request.
  • Cache contexts and cache tags solve different problems and are both sub-keys of #cache — contexts vary the cache key up front, tags invalidate existing entries after the fact.

Coming up next

Caching makes a slow module fast — but sometimes the right answer isn't to cache work, it's to defer it entirely: hand a task off to run later, in the background, outside the request/response cycle a visitor is waiting on. That's exactly what the next topic covers: Drupal's Queue API, Batch API, and cron system for handling long-running or deferred work.