Optimizing cache bins (e.g., render, data) for performancefor Drupal 8 , 9 , 10 , and 11

Last updated :  

Not all cached data is alike. A rendered page fragment, a piece of discovered plugin metadata, and a small application-level counter all get cached — but they're read and written at wildly different rates and sizes. Drupal keeps them apart in separate cache bins specifically so you can tune, monitor, or clear one category without touching the others.

What you'll learn in this lesson

  • What a cache bin actually is under the hood
  • The bins you'll encounter most often, and what each one holds
  • How to target a specific bin from your own code

The bins you'll meet constantly

  • cache.default (bin name default) — the general-purpose bin most custom module code writes to when it doesn't specify otherwise.
  • cache.render — cached render arrays and page fragments; this is the bin most affected by the tags/contexts/max-age metadata from earlier lessons.
  • cache.data — general structured data your code decides to cache, distinct from render output.
  • cache.discovery — plugin definitions, hook implementations, and other "what exists on this site" metadata, rebuilt on drush cr.
  • cache.bootstrap — data needed extremely early in the request lifecycle, before most of Drupal has even loaded.

Using a specific bin from your own code

// Inject the specific bin as a service, e.g. in a form's create():
$form->cacheBackend = $container->get('cache.default');

// Then use it exactly like any other cache backend:
$this->cacheBackend->set('my_module_expensive_result', $data, CacheBackendInterface::CACHE_PERMANENT);
$cached = $this->cacheBackend->get('my_module_expensive_result');

This is the exact pattern demonstrated in the Examples module's cache_example — a form injects cache.default as a service and uses the ordinary get()/set() cache-aside pattern against it. If you had a good reason to keep a particular module's cached data physically separate (heavy write volume, a need to clear it independently, or database-level tuning specific to that workload), you could define your own custom bin in a *.services.yml file instead of reusing cache.default.

Most modules never need a custom bin. Defining a new cache bin is an operational decision — it's another table (with the default database cache backend) to monitor, size, and maintain. Reach for cache.default unless you have a concrete, measured reason a specific workload needs isolation.

Quick check: after `drush cr`, your custom module's expensively-computed cached result is gone, even though you thought it was permanent. What likely happened? (A full `drush cr` clears every bin, including `cache.default` — "permanent" in the Cache API means "doesn't expire on its own," not "survives a full cache rebuild." Only tag-based invalidation and time-based expiry are things your code controls; a full rebuild clears everything regardless.)

Key takeaways

  • Cache bins separate different categories of cached data (render, discovery, bootstrap, general-purpose) so each can be reasoned about and tuned independently.
  • Most custom code should use cache.default unless there's a concrete, measured reason to isolate a workload into its own bin.
  • CACHE_PERMANENT means "no automatic time-based expiry," not "immune to a full cache rebuild" — drush cr clears every bin.

Coming up next

To close out this section, we look at entity.lazy_builder — a mechanism for deferring entity rendering work until it's actually needed, rounding out the toolkit alongside BigPipe and cache bins.