AI
min read
Last update on

Understanding Drupal’s cache API - core concepts and architecture

Understanding Drupal’s cache API - core concepts and architecture
Table of contents

Drupal’s Cache API plays a central role in delivering performance at scale. It allows developers to store and reuse data across requests, reducing load on the database and speeding up response times. With the right strategy, caching can reduce database queries by as much as 95%, while keeping content accurate and dynamic through smart invalidation.

This article breaks down the key components of Drupal’s caching system, cache bins, tags, contexts, and max-age, and shows how they work together in real-world scenarios. 

We’ll also walk through a practical example: fetching data from a third-party API, storing it with the Cache API, and managing its lifecycle using Drupal’s built-in tools. Whether you’re improving backend performance or fine-tuning user experience, the Cache API gives you precise control over how and when content is served.

Cache API fundamentals

Drupal’s Cache API provides a standardized mechanism to store, retrieve, and invalidate cached data. By abstracting the underlying storage system, you write the same code regardless of whether the data is stored in a database, memory, or even a remote backend like Redis.

Key principles of the cache API

  • Storage abstraction: Store cache data in a variety of backends.
  • Consistent interface: Use standard methods across cache implementations.
  • Intelligent invalidation: Utilise a tag-based system to clear only what’s necessary.
  • Context awareness: Cater to variations such as user roles, language preferences, and more.
  • Time-based expiration: Control how long an item remains in cache.

Core Interfaces and Methods

Before diving into custom cache implementations, it is important to understand Drupal’s foundational interface: CacheBackendInterface. This interface lays out the standard methods for all caching backends in Drupal. It defines how cache items are stored, fetched, and invalidated. In essence, it’s the contract that every cache implementation must adhere to.

For more detailed technical reference, check out the official CacheBackendInterface documentation, which explains all the methods and recommended usage patterns.

Some of the key methods include:

  • get($cid, $allow_invalid): Retrieves a single cached item.
  • getMultiple($cids, $allow_invalid): Efficiently retrieves multiple items.
  • set($cid, $data, $expire, $tags): Stores an item in the cache.
  • delete($cid): Removes a specific cache entry.
  • deleteAll(): Clears an entire cache bin.
  • invalidate($cid): Marks an item as invalid without removing it outright.
  • invalidateTags($tags): Invalidates all items associated with certain tags.
  • removeBin(): Completely removes a cache bin.

Understanding these methods lays the groundwork for implementing effective caching strategies in your modules.

A Practical use case: caching third-party API data

To bring theory into practice, let’s consider a common scenario: retrieving weather information from a third-party API, caching the result, and ensuring that our cache is sensitive to changes by using cache tags, contexts, and max-age parameters.

Use case description

Imagine you have a module that fetches weather data from a public API. Because the API call is relatively expensive in terms of time and resources, you want to cache the results. However, the weather might change throughout the day, so you need to balance freshness with performance by setting an appropriate expiration (max-age) and associating cache tags so that when the weather information is updated (either manually or via another process), you can invalidate the cache selectively.

Sample implementation

Below is a sample implementation. This style is more in line with typical Drupal practices.

/**
  * Fetches weather data from a third-party API and caches the result.
  *
  * @param int $location_id
  *   The identifier for the location.
  *
  * @return mixed
  *   The weather data.
  */
 function mymodule_get_weather_data($location_id) {
   // Build a unique cache ID for the specific location.
   $cid = 'mymodule:weather:' . $location_id;

   // Load the default cache bin (you could also use a custom bin if needed).
   $cache = \Drupal::cache('data');

   // Attempt to retrieve the cached weather data.
   if ($cache_item = $cache->get($cid)) {
     // Cache hit – return the cached data.
     return $cache_item->data;
   }

   // Cache miss – fetch weather data from the third-party API.
   $api_url = "https://api.example.com/weather?location={$location_id}";
   $response = file_get_contents($api_url);

   // Assuming the API returns JSON data.
   $weather_data = json_decode($response, TRUE);

   // Define cache tags to facilitate targeted invalidation.
   // For example, if weather data for a location is updated,
   // a process can invalidate the tag "weather:{$location_id}"
   $tags = [
     'weather:' . $location_id,
   ];

   // Define cache contexts if the data should vary.
   // If, for instance, the weather display should vary based on the user's timezone.
   $contexts = [
     'url.path', // Example context based on the URL.
   ];

   // Define a max-age of 30 minutes to ensure data freshness.
   $max_age = 1800; // Seconds.

   // The expiration timestamp using the current time.
   $expire = time() + $max_age;

   // Cache the API data.
   $cache->set($cid, $weather_data, $expire, $tags);

   // Optionally, attach cache context metadata when building a render array.
   // This could be returned along with the data, or directly applied in a controller.
   // Here’s an example of preparing a render array:
   $build = [
     '#theme' => 'weather_display',
     '#data' => $weather_data,
     '#cache' => [
       'contexts' => $contexts,
       'tags' => $tags,
       'max-age' => $max_age,
     ],
   ];

   return $build;
 }

Explaining the flow

  1. Cache ID creation:
    A unique cache ID ($cid) is generated using both a module-specific prefix and the location ID, ensuring that the data is correctly segmented.
  2. Cache loading:
    Instead of injecting the cache service into our function, we fetch it within the function using Drupal’s service container. This keeps our sample concise and directly illustrates the process.
  3. Cache retrieval:
    We check for an existing cache entry first. On a hit, the function returns the cached data immediately, avoiding the need to call the remote API.
  4. Fetching data and setting cache:
    On a cache miss, the function makes an API call using PHP’s file_get_contents() (a simple method for demonstration). The retrieved data is then decoded from JSON and cached.
  5. Applying cache tags and contexts:
    The cache entry is stored along with a specific cache tag, which allows us to invalidate all related cache entries (e.g., when weather data for that location is updated). Likewise, cache contexts (such as URL paths) can be defined when rendering data, ensuring that variations are properly handled.
  6. Cache expiration:
    The max-age is set to 30 minutes to balance between data freshness and performance benefits. (Note: This is assuming that data will be fresh for the next 30 minutes. This can vary on type of data being fetched.)

Cache metadata explained

Cache metadata is crucial in Drupal’s caching strategy. It controls how and when cached content should be invalidated.

Cache tags

Cache tags are string identifiers attached to cache entries. They track dependencies so that when a piece of content changes, related cache entries can be invalidated selectively. Consider this example:

$node_id = 5;
 $tags = [
   'node:' . $node_id,
   'node_list',
 ];

 \Drupal::cache('data')->set('example:node_summary:' . $node_id, $summary, Cache::PERMANENT, $tags);

Later, updating node 5 or the overall node list could trigger invalidation using these tags:

// Invalidate the cache for node 5.
 \Drupal\Core\Cache\Cache::invalidateTags(['node:5']);

 // Invalidate caches for all node listings.
 \Drupal\Core\Cache\Cache::invalidateTags(['node_list']);

For more details on cache tags, refer to the official Drupal caching documentation.

Cache contexts

Cache contexts tell Drupal how the same cache entry might vary based on different runtime conditions (e.g., different users, URLs, or themes). When you build a render array, it might look like this:

$build = [
   '#markup' => $this->t('Hello, @user', ['@user' => $username]),
   '#cache' => [
     'contexts' => ['user'],
     'tags' => ['user:' . $user_id],
     'max-age' => 3600,
   ],
 ];

Max-Age

The max-age setting defines the time period after which the cached data should be considered stale. For example:

\Drupal::cache('data')->set(
   'example:weather_data',
   $weather_data,
   time() + 1800,  // Cache expires in 30 minutes.
   ['weather:' . $location_id]
 );

Special values to note:

  • Cache::PERMANENT: The item never expires automatically, relying solely on tag-based invalidation.
  • 0: Indicates that an item should not be cached at all. Additionally, setting max-age to 0 in a render array will bubble up to the page level, effectively disabling caching for the entire page. This behaviour and its implications will be covered in more detail in future blog posts.

For a more in-depth discussion on cache metadata, please check the official Drupal caching API documentation.

Cache bins structure

Drupal organises cached data into separate “bins.” Each bin is a distinct storage container, designed to optimise different types of cache entries:

Bin Name Purpose When Used
bootstrap Essential configuration During Drupal bootstrap
config Configuration objects When working with config
default General-purpose cache For module-specific data
discovery Plugin information When discovering plugins
dynamic_page_cache Page fragments During page rendering
entity Entity data When loading entities
menu Menu trees When rendering menus
render Rendered output During the rendering process

For example, using a specific bin may look like this:

// Get the cache bin for rendered content.
 $render_cache = \Drupal::service('cache.render');

 // Store rendered output in the cache.
 $render_cache->set('example:rendered_element', $rendered_output, Cache::PERMANENT, $cache_tags);

Series navigation

This post is part of our comprehensive 10-part series on Drupal caching:

  • Introduction to Drupal Caching
  • Understanding Drupal’s Cache API (You are here)
  • Choosing the Right Cache Backend for Your Drupal Site (Coming soon)
  • Mastering Page and Block Caching in Drupal (Coming soon)
  • Optimising Drupal Views and Forms with Caching (Coming soon)
  • Entity and Render Caching for Drupal Performance (Coming soon)
  • Building Custom Caching Services in Drupal (Coming soon)
  • Implementing Custom Cache Bins for Specialised Needs (Coming soon)
  • Advanced Drupal Cache Techniques (Coming soon)
  • Drupal Caching Best Practices and Performance Monitoring (Coming soon)

What’s next

Now that you’ve seen how Drupal’s Cache API works in practice, from understanding cache bins and metadata to implementing custom caching with third-party data, you’re ready to go a step deeper. In the next part of this series, we’ll shift focus to choosing the right cache backend for your Drupal site.

Different backends come with different trade-offs. Whether you're considering the default database cache, memory-based systems like Memcached or Redis, or cloud-managed solutions, the backend you choose directly affects cache hit rates, memory usage, and site responsiveness. 

We’ll break down how each option works, when to use them, and how to configure them correctly in a production environment. Stay tuned as we continue building toward a complete understanding of caching in Drupal, one layer at a time.

Written by
Editor
Ananya Rakhecha
Tech Advocate