Understanding Caching in Drupal, Pt. 1
In Drupal 11, caching is not just a "setting" you turn on; it is a sophisticated, multi-layered system that handles everything from full-page snapshots for visitors to granular data objects for developers.
At a high level, the system is designed to ensure that if a piece of data has already been calculated once, it should never be calculated again unless it actually changes.
1. The Core Caching Modules
Drupal 11 uses three primary internal mechanisms to handle different types of traffic and content.
| Mechanism | Who it's for | How it works |
|---|---|---|
| Internal Page Cache | Anonymous Users | Caches the entire HTML page. It is lightning fast because it bypasses almost all of Drupal's processing. |
| Dynamic Page Cache | All Users | Caches the "skeleton" of a page. It strips out personalized parts (like "Welcome, [Name]") and replaces them with placeholders so the rest of the page can be reused. |
| BigPipe | Authenticated Users | A delivery strategy. It sends the "skeleton" immediately so the user sees something instantly, then "streams" the personalized parts into the placeholders as they finish rendering. |
2. The "Brain": Cacheability Metadata
The most important part of Drupal 11 caching is metadata. Every element in Drupal (a block, a node, a menu) "tells" the system three things about itself:
Cache Tags (The "What")
These are dependencies. If you have a block showing a "Recent Articles" list, it is tagged with node_list.
* The Power: If you create a new article, Drupal automatically sends a signal: "Invalidate everything tagged with node_list." * Result: You never have to manually clear your cache when you update content.
Cache Contexts (The "Who/Where")
These define variations of a cache item. A "My Profile" block shouldn't be the same for every user.
* Examples: user (different per person), languages (different per language), or url.query_args (different based on search filters).
* The Power: Drupal creates separate "buckets" for each context automatically.
Cache Max-Age (The "How Long")
This is a simple time limit (in seconds).
* Default: Most things are "Permanent" (-1), relying on Cache Tags to tell them when to expire.
* Use Case: A "Weather" block might have a max-age of 3600 (one hour) because the data becomes stale regardless of site updates.
3. High-Level Architecture
Drupal organizes data into Cache Bins. You can think of these as separate drawers in a filing cabinet:
* render: For bits of HTML (blocks, entities).
* data: For raw data or API responses.
* discovery: For system-level things like "Which modules are installed?".
By default, these are stored in your database (the cache_* tables). However, for high-traffic sites, it is common to swap the storage backend to Redis or Memcached so the data is served from RAM instead of the disk.
4. How the Request Flow Works
When a user visits a page in Drupal 11, the system checks the layers in this order:
* Is this an anonymous user? Check the Internal Page Cache. If it's a "HIT," send the HTML and stop.
* Is there a Dynamic Page Cache entry? If yes, grab the "skeleton."
* Fill the gaps: Render any parts that couldn't be cached (using BigPipe if enabled).
* Save for later: If anything new was rendered, store it in the appropriate cache bin for the next visitor.
Would you like me to explain how to debug these caches using browser headers, or perhaps how to implement caching in your own custom code?
To help you master Drupal 11 caching, we can look at how to debug it from the outside (the browser) and how to code for it from the inside.
1. Debugging Caches via Browser Headers
The easiest way to see if your caching is working is to inspect the HTTP Headers. Drupal 11 sends back "X-Drupal-Cache" and "X-Drupal-Dynamic-Cache" headers if you enable the http.response.debug_cacheability_headers: true parameter in your services.yml file.
What to look for:
* HIT: The page was served entirely from the cache. This is the goal for performance.
* MISS: The page had to be built from scratch. This happens on the first load after a cache clear.
* UNCACHEABLE: Usually happens when a "Session" is started (like adding an item to a cart) or if a specific piece of code forbids caching.
2. Implementing Caching in Custom Code
If you are building a custom block or controller, you must pass your "Cacheability Metadata" back to Drupal so it knows how to handle your content.
The Render Array Approach
In Drupal, you don't "save" to the cache manually most of the time. Instead, you add a #cache key to your output array:
$build['my_custom_block'] = [
'#markup' => $this->t('Hello, @name!', ['@name' => $username]),
'#cache' => [
// 1. Context: Varies by the user viewing the page
'contexts' => ['user'],
// 2. Tag: Clear this if "node 5" is updated
'tags' => ['node:5'],
// 3. Max-age: Expire in 1 hour regardless
'max-age' => 3600,
],
];
3. The "Bubbling" Principle
One of the most "magic" parts of Drupal caching is Bubbling.
If a small block at the bottom of your sidebar has a cache context of user, Drupal "bubbles" that requirement up to the entire page. Even if the main article is the same for everyone, the fact that one small block is personalized means the entire page becomes personalized (unless using BigPipe).
> Note: This is why you should always be as specific as possible. Don't use a user context if you only need a user.permissions context!
>
4. When to Manually Clear Caches
As a developer, you'll often use drush cr (Cache Rebuild). In Drupal 11, you generally only need to do this when:
* You've changed a YAML file (like .routing.yml or .info.yml).
* You've added a new Plugin (like a new Block class).
* You've changed a Twig template (if Twig caching is on).
For content changes, the Cache Tags system handles the clearing for you automatically.
Would you like me to show you how to set up a local development environment (like DDEV) to test these caching layers safely?
To test Drupal 11 caching layers effectively, you need an environment where you can flip between "Production-like" (caches on) and "Development" (caches off) modes easily. DDEV is the industry standard for this.
1. Setting up a Drupal 11 Sandbox with DDEV
If you don't have a site yet, run these commands in your terminal to spin up a fresh Drupal 11 instance:
mkdir my-drupal-site && cd my-drupal-site
ddev config --project-type=drupal11 --docroot=web
ddev start
ddev composer create-project "drupal/recommended-project:^11"
ddev drush site:install --account-name=admin --account-pass=admin -y
ddev launch
2. Configuring "Development Mode"
By default, Drupal caches everything, which makes it hard to see code changes. To see your Cache Headers and Twig changes immediately, follow these steps:
A. Enable the Local Settings
In web/sites/default/settings.php, find and uncomment these lines (usually at the very bottom):
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
include $app_root . '/' . $site_path . '/settings.local.php';
}
B. Disable Caches for Testing
Open web/sites/default/settings.local.php. To see your code changes without running drush cr every time, ensure these lines are active:
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
// This "nulls" the render and dynamic page caches so you see changes live
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
3. Viewing Cache Debug Headers
To understand why a page is or isn't caching, you need to see the "X-Drupal-Cache" headers in your browser.
* Open web/sites/development.services.yml.
* Add this under the parameters: section:
parameters:
http.response.debug_cacheability_headers: true
* Run ddev drush cr to apply the change.
* Open your browser's Developer Tools > Network tab. Click on the main page request and look for:
* X-Drupal-Cache-Tags: Shows every content dependency for that page.
* X-Drupal-Cache-Contexts: Shows what data varies (e.g., user.permissions, theme).
4. Pro-Tip: Testing Performance
Once you're finished coding, comment out the cache.backend.null lines in your settings.local.php. This puts the site back into a "Production" state. You can then use the network tab to verify that your page loads are hitting the cache (X-Drupal-Cache: HIT).
Getting started with DDEV for Drupal development
This video provides a comprehensive walkthrough of setting up a local Drupal environment using DDEV, which is the perfect foundation for testing the caching concepts we've discussed.
Would you like me to write a custom PHP script for a "Cache-aware" block that you can drop into your new DDEV site to test?
YouTube video views will be stored in your YouTube History, and your data will be stored and used by YouTube according to its Terms of Service
Here is a simple, custom "Cache-Aware" block you can create in your DDEV environment. This block will display the current time and a random number, but we will use Cache Metadata to control exactly how often it updates.
1. Create the Directory Structure
In your DDEV terminal (or local file explorer), create the following folder path inside your Drupal root:
web/modules/custom/cache_test/src/Plugin/Block/
2. Create the Block File
Create a file named CacheTestBlock.php in that folder and paste this code:
<?php
namespace Drupal\cache_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Cache Test' Block.
*
* @Block(
* id = "cache_test_block",
* admin_label = @Translation("Cache Test Block"),
* )
*/
class CacheTestBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => $this->t('The time is @time and your lucky number is @num.', [
'@time' => date('H:i:s'),
'@num' => rand(1, 100),
]),
'#cache' => [
// This block will vary for every unique user session.
'contexts' => ['session'],
// This block will expire every 30 seconds.
'max-age' => 30,
// Manual tag to allow us to clear it specifically.
'tags' => ['my_custom_tag'],
],
];
}
}
3. Register the Module
Create web/modules/custom/cache_test/cache_test.info.yml:
name: Cache Test
type: module
description: 'A simple module to test Drupal 11 caching.'
core_version_requirement: ^11
package: Custom
How to test this in DDEV
* Enable the module: Run ddev drush en cache_test -y.
* Place the block: Go to /admin/structure/block in your browser and place the "Cache Test Block" in your Sidebar or Content region.
* Observe Max-Age: Refresh the page. The time and number should stay the same for 30 seconds, even if you refresh. After 30 seconds, it will update once.
* Observe Contexts: Open the site in an Incognito/Private window. You will see a different time/number than your main browser because the session context is different.
* Test Manual Invalidation: Want to force just this block to update without clearing the whole site cache? Run this DDEV command:
ddev drush eval "\Drupal\Core\Cache\Cache::invalidateTags(['my_custom_tag']);"
Refresh your page, and you’ll see the block has updated!
The Resulting Data Flow
By using invalidateTags, you are telling Drupal: "I don't care about the 30-second timer; anything that relies on 'my_custom_tag' is now old news. Rebuild it." This is exactly how Drupal handles updating a "Latest News" list when a new article is saved.
Would you like me to show you how to swap your DDEV database cache for Redis to see how enterprise-level caching feels?
Moving your cache from the database to Redis is a common "pro" move. It offloads the heavy lifting from your MySQL database to an in-memory store, which is significantly faster.
In DDEV, this is a streamlined process because there is an official "add-on" for it.
1. Install the Redis Service in DDEV
Run these commands in your project terminal:
# 1. Add the Redis service to your DDEV environment
ddev add-on get ddev/ddev-redis
# 2. Restart DDEV to spin up the new Redis container
ddev restart
2. Install the Drupal Redis Module
Now that the server is running, Drupal needs the code to talk to it.
# 1. Download the module via Composer
ddev composer require drupal/redis
# 2. Enable the module
ddev drush en redis -y
3. Configure Drupal to Use Redis
DDEV's Redis add-on automatically creates a file at web/sites/default/settings.ddev.redis.php. You just need to tell your main settings.php to use it.
Open web/sites/default/settings.php and add this at the very bottom:
// Use Redis for caching if in a DDEV environment.
if (getenv('IS_DDEV_PROJECT') == 'true' && file_exists(__DIR__ . '/settings.ddev.redis.php')) {
include __DIR__ . '/settings.ddev.redis.php';
}
> Warning for Drupal 11: There is currently a known bug in some D11 versions where Redis can cause a "White Screen of Death" (WSOD) during the initial bootstrap if not configured carefully. Using the official DDEV helper file above is the safest way to avoid this.
>
4. Verify It's Working
You can now "look inside" the Redis memory to see Drupal writing to it.
* Clear Caches: ddev drush cr
* Browse the site: Click around a few pages to generate some cache data.
* Check Redis: Run the following command to see the cache keys:
ddev redis-cli KEYS *
You should see a long list of keys starting with drupal.redis.... If you see these, congratulations! Your site is now running on an enterprise-grade caching backend.
How Redis Fits into the Architecture
In this setup:
* MariaDB (MySQL): Stores your nodes, users, and configuration (Long-term storage).
* Redis: Stores your render arrays and page fragments (Volatile, fast-access memory).
Would you like to see how to use "Cache Tags" to automatically clear specific Redis entries when you update an external API
In Drupal 11, Cache Tags are the "magic glue" that links your cached content to the underlying data. They allow for surgical invalidation: instead of clearing the entire site cache when one thing changes, Drupal only clears the specific pieces of the site that depend on that data.
1. The Naming Convention
Cache tags are strings, usually formatted as thing:identifier. Drupal core provides many of these automatically:
| Tag | What it represents |
|---|---|
| node:5 | A specific piece of content (Node ID 5). |
| node_list | Any list of nodes (e.g., a View or a "Recent News" block). |
| config:system.site | Site-wide configuration (like the Site Name). |
| user:1 | A specific user's profile data. |
| library_info | CSS or JavaScript asset changes. |
2. Using Tags in Custom Code
When you build a custom element, you "attach" tags to it. This tells Drupal: "If any of these tags are invalidated, throw this cache away and rebuild it."
$build['my_content'] = [
'#markup' => '<p>Hello World</p>',
'#cache' => [
'tags' => [
'node:12', // Invalidate if Node 12 is edited
'my_custom_tag', // Invalidate if we manually trigger this tag
],
],
];
3. Manual Invalidation (The "Trigger")
If you are importing data from an external API or a custom database table, Drupal won't know when that data changes. You have to tell it by "invalidating" the tag in your custom logic:
use \Drupal\Core\Cache\Cache;
// Assume we just updated some external data...
// This will clear EVERY cache item across the whole site that has this tag.
Cache::invalidateTags(['my_custom_tag']);
4. How the "Invalidation Flow" Works
Imagine a page with a sidebar block showing "Top 3 Articles."
* Rendering: Drupal renders the block and sees it has the tag node_list.
* Storage: Drupal saves the HTML of that block in Redis/Database, associated with the node_list tag.
* The Event: An editor creates a new article.
* Automatic Signal: Drupal's Node system automatically calls Cache::invalidateTags(['node_list']).
* Clean up: Drupal looks at the cache storage and deletes everything marked with node_list.
* Refresh: The next user to visit the page sees the block is missing from cache, so Drupal regenerates it with the new article included.
5. Pro Tip: The node_list vs. node:ID distinction
* Use node:5 when you are displaying the actual body or title of Node 5.
* Use node_list when you are displaying a list where the count or selection of nodes might change (like a search page or a "Latest News" block).
If you only used node:5 on a "Latest News" list, and you added a new article (Node 6), the list wouldn't update because Node 5 hasn't changed! This is why node_list is critical for dynamic collections.
Verifying Tags in DDEV
Since we enabled debug headers earlier, you can see this in action:
* Open your DDEV site in Chrome.
* Open Inspect > Network.
* Click the main request and find X-Drupal-Cache-Tags.
* You will see a massive list of tags. This is the "dependency map" for the entire page.
Would you like me to show you how to add a custom Cache Tag to a View so it clears whenever you update an external API?
In Drupal 11, Views are the most common way to display lists of content. By default, Views automatically handle cache tags for Drupal content (like nodes), but if your View relies on external data (like a weather API or a stock ticker), you need to manually tell the View to "listen" for a custom tag.
Here is how you can inject a custom cache tag into a View and then trigger it.
1. The Code: Injecting the Tag
You can use hook_views_post_render() in your .module file to attach a custom tag to a specific View.
File: web/modules/custom/cache_test/cache_test.module
/**
* Implements hook_views_post_render().
*/
function cache_test_views_post_render(\Drupal\views\ViewExecutable $view, &$output, \Drupal\views\Plugin\views\cache\CachePluginBase $cache) {
// Check if this is the specific View you want to target.
if ($view->id() == 'my_external_api_view') {
// Attach your custom tag.
$output['#cache']['tags'][] = 'external_api_update';
}
}
By doing this, any time this View is rendered, it is "stamped" with the tag external_api_update. It will stay cached forever (or until its max-age) unless that tag is invalidated.
2. The Trigger: Invalidating when API data changes
Now, imagine you have a scheduled task (a Cron job) that fetches new data from an API. Once the new data is saved to your database, you want the View to refresh immediately.
use \Drupal\Core\Cache\Cache;
/**
* A hypothetical function that runs when your API data is updated.
*/
function cache_test_update_api_data() {
// ... Logic to fetch and save your API data ...
// Now, tell Drupal to clear everything tagged with our custom tag.
Cache::invalidateTags(['external_api_update']);
\Drupal::messenger()->addMessage('API Data updated and View cache cleared!');
}
3. How to verify in DDEV
Once you have implemented the hook above:
* Clear Caches: Run ddev drush cr.
* Visit the Page: Go to the URL where your View is located.
* Inspect Headers: In your browser's Network tab, look at the X-Drupal-Cache-Tags header.
* Find your tag: You should see external_api_update listed among the other tags (like node_list, config:views.view.my_view, etc.).
4. Why this is better than "Clearing All Caches"
On a production site, running drush cr is "the nuclear option." It deletes all cached data, forcing the server to rebuild every single block, menu, and page from scratch, which causes a massive spike in CPU usage.
By using Cache::invalidateTags(['external_api_update']):
* The View is cleared.
* The Page containing the View is cleared.
* Everything else (the Main Menu, the Footer, other Blocks, other Pages) stays safely in the Redis cache.
This keeps your site fast for users while ensuring your data is never stale.
Would you like me to show you how to set up a "Cache Context" so that a View shows different results based on a URL query parameter (like ?category=sports)?
Recent content
-
1 hour 5 minutes ago
-
1 week ago
-
1 week 1 day ago
-
1 week 1 day ago
-
1 week 1 day ago
-
1 week 6 days ago
-
2 weeks ago
-
2 weeks 1 day ago
-
2 weeks 1 day ago
-
2 weeks 2 days ago