Drupal is a mature and flexible content management system (CMS) trusted for building structured content models and managing complex workflows.
On the front end, Next.js is a widely adopted React framework known for its speed, developer experience, and production-grade features. With the release of Next.js 15, the new App Router brings powerful updates, including built-in support for async/streaming, granular caching, and improved routing patterns.
When connecting a decoupled Drupal backend to a Next.js 15 frontend, handling authentication becomes a key step, especially for gated content, personalisation, or editorial workflows.
This blog covers how to implement authentication in a headless Drupal + Next.js 15 setup using the latest App Router features and best practices around API routes, sessions, and token handling.
Before starting, ensure you have Node.js installed. Then, run the following command to create a new Next.js 15 project:
npx create-next-app@latest my-nextjs-app --ts --experimental-app
cd my-nextjs-app
npm install
To start the development server:
npm run dev
Your Next.js app should now be running at http://localhost:3000.
my-nextjs-app/
├── app/ # App Router directory
│ ├── layout.tsx # Root layout (applies to all pages)
│ ├── page.tsx # Home page (/)
│ ├── about/ # Example route: /about
│ │ └── page.tsx
│ └── api/ # API routes
│ └── user/route.ts
│
├── components/ # Reusable components (e.g., Header, Button)
│ └── Header.tsx
│
├── styles/ # Global and module CSS files
│ ├── globals.css
│ └── Home.module.css
│
├── public/ # Static assets (images, favicon, etc.)
│ └── favicon.ico
│
├── next.config.js # Next.js configuration
├── tsconfig.json # TypeScript configuration (if using TS)
└── package.json # Project metadata and dependencies
Install Drupal new setup
To enable API-based communication between Next.js and Drupal, install the JSON: API module:
ddev composer require drupal/jsonapi
ddev drush en jsonapi -y
To allow Next.js to access Drupal’s API, update services.yml:
cors.config:
enabled: true
allowedOrigins: ['*']
allowedHeaders: ['Content-Type', 'Authorisation']
allowedMethods: ['GET', 'POST', 'OPTIONS', 'PATCH', 'DELETE']
Clear the cache:
ddev drush cr
Next.js 15 introduces improved async handling and caching. To fetch content from Drupal, install the required packages:
npm install next-drupal drupal-jsonapi-params
Set the variables
# Required
NEXT_PUBLIC_DRUPAL_BASE_URL=https://my-site.ddev.site/
NEXT_IMAGE_DOMAIN=my-site.ddev.site
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Update the image dome in next.config.ts file
Import type { NextConfig } from “next”;
const nextConfig: NextConfig = {
images: {
domains: ["my-site.ddev.site"], // Allow images from this domain
remotePatterns: [
{
protocol: "https",
hostname: "my-site.ddev.site",
pathname: "/sites/default/files/**",
},
],
},
};
export default nextConfig;Then, create a client to fetch data using the new fetch API with caching:
import { NextDrupal } from "next-drupal"
import { DrupalJsonApiParams } from "drupal-jsonapi-params";
const drupal = new NextDrupal(process.env.NEXT_PUBLIC_DRUPAL_BASE_URL);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; // To fix the development fetch error
export async function fetchArticles() {
const params = new DrupalJsonApiParams()
.addFields("node--article", ["title", "id", "body", "field_image", "field_tags"])
.addInclude(["field_image", "field_tags"])
.addSort("created", "DESC");
const response = await drupal.getResourceCollection("node--article", {
params: params.getQueryObject(),
cache: "force-cache" // Ensures fast loading
});
return response;
}
Rendering the fetched content in Next.js 15:
import { fetchArticles } from "../lib/drupal";
import Image from "next/image";
export default async function Page() {
const data = await fetchArticles();
console.log(data);
return (
<div className="">
{data.map((article) => (
<div key={article.id || article.title}> {/* ✅ Add key to top-level item */}
<h2 className="article-title">{article.title}</h2>
{article.field_image?.uri?.url && (
<Image
src={`${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${article.field_image.uri.url}`}
alt={article.title}
width={400}
height={250}
className="image"
/>
)}
{article.body?.value ? <p>{article.body.value}</p> : <p></p>}
{article.field_tags?.length > 0 && (
<ul>
{article.field_tags.map((tag, index) => (
<li key={tag.id || `${tag.name}-${index}`}>{tag.name}</li>
))}
</ul>
)}
</div>
))}
</div>
);
}
By integrating Drupal’s headless capabilities with Next.js 15 and its modern App Router, teams gain a powerful architecture that combines content flexibility with frontend performance.
Drupal continues to provide a structured, API-first backend for complex content models and editorial workflows, while Next.js 15 brings faster rendering through granular caching, improved async handling, and enhanced developer ergonomics.
This setup creates a future-ready foundation for scaling digital experiences. You can introduce personalisation, real-time updates, static generation for anonymous content, and even edge rendering with Next.js middleware, which also aligns well with the growing demand for composable architectures, enabling easy integration with third-party services like analytics, commerce, or AI-based recommendations.
As headless adoption grows, pairing Drupal with Next.js 15 offers a stable yet forward-looking path, supporting editorial flexibility, high performance, and extensibility for evolving digital products.

Drupal’s new Experience Builder (XB) is changing how teams create and manage content. It gives editors and developers a shared, visual interface to design pages quickly, without writing code.
In this blog, we’ll explain what Drupal’s Experience Builder is, how it’s different from Layout Builder, and why its Code Components are becoming a key part of modern Drupal development.
Experience Builder is Drupal’s next-generation visual page builder. It helps non-technical users create, edit, and manage pages using a simple drag-and-drop interface.
It’s built around a component-based system, so every part of a page — a banner, a card, or a section — can be reused and customised visually.
This tool is part of Drupal’s Starshot Initiative, which focuses on making site building faster, easier, and more consistent for teams.
While both tools help shape page layouts in Drupal, Layout Builder and Experience Builder serve different goals.
Layout Builder focuses on structuring content types. It’s built for developers and site builders who define layouts that apply across similar pages, such as all blog posts or articles. It’s backend-focused and block-based, which gives it precision but limits flexibility for non-technical users.
Experience Builder, on the other hand, is designed for editors and marketers who want more visual control. It lets users build full pages through a drag-and-drop interface and see updates instantly. Instead of relying on Twig templates and PHP blocks, it uses modern React-based Code Components that can be customised and reused across pages.
In short, Layout Builder defines the structure; Experience Builder defines the experience. It takes Drupal’s structured approach and adds a modern, component-driven workflow that feels faster, more visual, and more flexible.
Experience Builder allows content editors to visually build and manage pages using a drag-and-drop interface. This means editors don’t need to write code to create or change page layouts — they can do it all visually and easily.
Code Components are fully customizable building blocks in Experience Builder (XB) that allow developers to create reusable UI elements using React, JSX, and Tailwind CSS.
They are built using Drupal’s Single Directory Components structure, which keeps markup, logic, and styles together for better maintainability.
While these components are coded by developers, they are designed to be user-friendly for content editors, who can drag and drop them into pages, configure props, and manage layout without writing any code.
They enable developers to define the structure, style, and configurable options (props and slots), making UI elements reusable and consistent. Content editors can visually manage and customise these components without needing to write code, speeding up page creation and ensuring design consistency across the site.
Common use cases include:
Note: Before creating Code Components, make sure Experience Builder (XB) is set up and ready to use on your Drupal site. Once everything is ready, you can start building components easily inside the builder.
Currently, XB supports only the Article content type.




Now, we will create a new Code Component called “Text”. This component will display text on the page. Later, we’ll make it editable so content editors can change the text directly in the Experience Builder without writing any code.






It uses React with Tailwind CSS. Currently, React version 18.3.1 and Tailwind CSS v4 are included.
You don’t need to import Tailwind manually — it’s already included globally.
Some helpful packages are pre-installed — 2 built-in and 3 third-party packages. Currently, only these packages are available for use.
Built-in packages:
Third-party packages: clsx, class-variance-authority (cva), tailwind-merge (twMerge)
Understanding the default code:
At this point, your component is static — it always shows the same text, and there's no way to customise it from the UI.
Now, let’s make this component dynamic!
To do that, we’ll use props — dynamic inputs that allow you to customise the component’s content and behaviour from the UI.
By adding props to your component and defining them under "Component data", these inputs will appear as editable fields for your component in the Experience Builder interface.
For example, if you expose a text prop, editors will be able to enter custom text directly from the UI while placing the Text component.
We can add any type of prop as needed — text, image, boolean, and more — based on the kind of content we want editors to customize. For our example, I’ll add a text-type prop to make the Text component dynamic and editable.
For our Text component, we will:
Note: The camelCased prop name must match the argument name in the component. (e.g., "Card variant" → cardVariant)



Besides props, Code Components also support slots. Slots are special placeholders inside a component where editors can add other components or content.
For example, a "Card" component can have slots for the header, body, and footer. Editors can easily drag and drop content or other components into those areas.
We take slots as inputs just like props, and render them inside the component to allow placing other components.



Some Tailwind classes are added inline in the component markup. You can directly apply Tailwind v4 classes this way.
There are also two CSS-related tabs available:
Important notes:







Code Components are just one part of how Experience Builder is changing how teams build in Drupal. What used to require multiple files, overrides, and backend coordination can now be done directly in the browser. It's faster, cleaner, and creates less back-and-forth between developers and editors.
But this is just the beginning.
Some of us at QED42 are already exploring what happens when AI enters the mix. The xb_ai_assistant module, now available on Drupal.org, generates page templates by intelligently placing available Single Directory Components (SDCs) based on simple prompts. While it works seamlessly with the Starshot demo theme (try it out instantly!), It’s also compatible with any theme. Just ensure your SDCs include proper descriptions for AI-assisted placement.
The module assembles layouts in real time and supports contextual editing, letting you tweak individual components directly on the page. See it in action here: Taking GenAI in Drupal to the Next Level, where we showcase prompt-based layout generation in Experience Builder.
But it’s not just about layouts. We’re also rethinking design workflows. In this LinkedIn post, we explore how AI can generate Code components directly from Figma designs. By leveraging Figma’s MCP, developers can share a Figma link and automatically generate code-ready components, skipping manual handoffs entirely.
The direction is clear: faster workflows, tighter collaboration, and more intelligent building blocks. As Experience Builder grows, it’s not hard to imagine a future where building with Drupal feels more like shaping ideas in real time with code, content, and design all connected.
Start small. Try a Code Component. Or test what’s possible with AI. Either way, this is a good moment to rethink how you build.
Q: Is the Experience Builder part of Drupal core?
Not yet. It’s an evolving community project under the Starshot initiative.
Q: Does it replace the Layout Builder?
No. It builds upon it with a more visual, component-driven experience.
Q: Can I use it with my existing theme?
Yes. It works with themes that support modern component structures.
The Drupal community is finding new momentum with AI as a practical extension of what Drupal does best: structured content, flexible architecture, and community-driven innovation.
In this blog, we focus on one of the most promising developments: AI-powered search.
Search has always been central to user experience. But traditional keyword matching falls short when users ask nuanced questions or use everyday language.
AI Search changes that by combining vector-based retrieval with large language models. It brings context, intent, and semantic understanding into the equation - helping users find what they mean, not just what they type.
We’ll cover how to integrate this capability into a Drupal site step by step. This includes setting up a vector database for storing semantic embeddings, connecting it with your Drupal content, and building a conversational assistant that can guide users through your site. We’ll also explore how prompt engineering allows you to shape the tone, accuracy, and depth of responses, giving you more control over how AI interacts with your content.
Whether you're running a public knowledge base, an internal documentation hub, or a highly structured content repository, this blog is meant to help you bring meaningful AI experiences into Drupal- thoughtfully and practically.
AI Search is a submodule of the AI module that extends the functionality of the contributed Search API AI module, offering seamless integration with Drupal’s Search API. It utilises vector databases and large language models (LLMs) to enable intelligent, semantic search capabilities.
By building on the popular Search API module, AI Search allows you to create and manage vector databases, enabling highly relevant and accurate retrieval of content based on terms, phrases, or even entire content pieces.
It uses Retrieval-Augmented Generation (RAG), where information is first looked up, usually from a vector database, and then sent to a large language model (LLM) along with a user's question or request. This helps the model provide much more accurate answers, especially about specific topics or content it may not already know or was not trained on.
The system works by breaking large pieces of content into smaller chunks and saving them in a vector database. Each chunk is also saved with extra metadata (such as title or other settings) to preserve its original meaning and context.
These chunks are converted into vectors — complex numerical representations of the content’s meaning. You can think of these numbers like advanced tags, each with varying strengths. For example, one number might indicate a slight relationship to transportation, while another might strongly relate to education.
When someone submits a query, the question is also converted into a vector. The system compares it to stored vectors to find the most relevant matches. This method is significantly more accurate than traditional keyword-based systems like regular databases or the SOLR Search API.
Install the following modules:
When choosing a vector database:





Use the Recipes content type (For this blog, we are using the Recipes content type for search as an example)

Click Save to finalize the index.

Index options explained
Go to the Views tab:
After indexing, view the data in Milvus or Zilliz Cloud to find your content being indexed.
Milvus Cloud:

In Zillis Cloud:


If results appear with similarity scores, the index is working correctly.
AI Agents and AI Assistants make AI Search more powerful and user-friendly. The AI Agent handles behind-the-scenes tasks like querying the vector database, filtering results, and managing tools like Retrieval-Augmented Generation(RAG). The AI Assistant acts as the front-end guide—chatting with users, interpreting their questions, and passing them to the agent. Together, they create a seamless, conversational search experience that understands user intent and delivers smarter, more relevant results. Hence we need to create and configure AI agent and AI assistant for AI search to work in AI chatbot.





The chatbot will now appear on the homepage and support AI-powered searches.

Drupal’s AI Search brings meaning to the center of search. It uses vector-based retrieval and large language models to understand intent, context, and relationships between words , not just match keywords.
This makes discovery smoother and more relevant. From recipe suggestions that adjust to user preferences, to module searches that surface the most useful tools, AI Search helps your site respond in smarter, more human ways.
It’s a shift toward more intuitive, helpful digital experiences, and just one of the ways AI is shaping what’s next for Drupal.
More updates coming soon in this series on AI and Drupal.

Parakeet-TDT-0.6B-v2 is a 600-million-parameter automatic speech recognition model designed for high-quality English transcription. Despite its relatively modest size compared to multi-billion parameter alternatives, this model delivers exceptional performance across a wide range of benchmarks.

The name “Parakeet” represents NVIDIA’s family of ASR models, while “TDT” refers to the innovative Token-and-Duration Transducer architecture that powers it. The “0.6B” indicates its parameter count (600 million), and “v2” signifies this is an improved second version of the model.
Key features of Parakeet-TDT-0.6B-v2 include:
Parakeet-TDT-0.6B-V2 is a 600-million-parameter ASR model designed specifically for high-quality English transcription. The model offers several standout features:
The model is based on NVIDIA’s FastConformer architecture with the Token Duration Transducer (TDT) decoder, combining cutting-edge design elements to achieve state-of-the-art results.

The Parakeet-TDT-0.6B-v2 model is built on the FastConformer encoder architecture, a highly optimised version of the standard Conformer model that has dominated speech recognition tasks in recent years. FastConformer introduces several key innovations that significantly enhance performance:

These modifications result in an encoder that is approximately 2.4–2.8 times faster than the regular Conformer encoder without significant quality degradation. Additionally, FastConformer supports efficient processing of long audio sequences through a linearly scalable attention mechanism inspired by the Longformer approach, which combines local attention with global tokens to maintain performance while reducing computational overhead.

A key innovation in the Parakeet-TDT-0.6B-V2 model is the Token Duration Transducer (TDT) decoder, which extends conventional RNN-Transducer architectures by jointly predicting:
This dual prediction system uses a joint network with two independently normalised outputs that generate distributions for tokens and their durations. During inference, the TDT can skip input frames based on predicted durations, making it significantly faster than conventional Transducers that process encoder output frame by frame.
The TDT approach offers several critical advantages:
The Parakeet-TDT-0.6B-V2 model demonstrates impressive performance across a variety of benchmarks, making it a top contender in the ASR space.
The model achieves remarkable accuracy across multiple datasets, as shown in the following table:
Dataset-specific WER scores include:
Parakeet-TDT-0.6B-v2 maintains strong performance even in noisy environments:
This robust performance across varying noise levels makes the model suitable for real-world applications where perfect acoustic conditions cannot be guaranteed.
One of the most remarkable aspects of Parakeet-TDT-0.6B-v2 is its processing speed. The model demonstrates an impressive real-time factor (RTF) of 3380 with a batch size of 128, meaning it can transcribe approximately 56 minutes of audio in just one second under optimal conditions.
Even with smaller batch sizes or on less powerful hardware, the model maintains strong performance, making it suitable for both real-time applications and batch processing scenarios.
When comparing Parakeet-TDT-0.6B-V2 with other state-of-the-art ASR models, several key advantages become apparent:


When compared to commercial ASR solutions like Google Speech-to-Text, Amazon Transcribe, and Microsoft Azure Speech, Parakeet-TDT-0.6B-v2 offers several advantages:
However, commercial solutions may offer additional features like speaker diarization, enhanced multilingual support, or tighter integration with their respective cloud ecosystems.
The model is available through NVIDIA’s NeMo toolkit and can be easily integrated into applications for inference or fine-tuning. Here’s a simple guide to get started:
First, you’ll need to install the NeMo toolkit and its ASR components:
pip install -U nemo_toolkit['asr']
To transcribe an audio file, you can use the following Python code:
import nemo.collections.asr as nemo_asr
# Load the model
asr_model = nemo_asr.models.ASRModel.from_pretrained(model_name="nvidia/parakeet-tdt-0.6b-v2")
# Download a sample audio file
# wget https://dldata-public.s3.us-east-2.amazonaws.com/2086-149220-0033.wav
# Transcribe the audio
output = asr_model.transcribe(['2086-149220-0033.wav'])
print(output[0].text)
If you need timestamp information along with the transcription:
output = asr_model.transcribe(['2086-149220-0033.wav'], timestamps=True)
# Access word-level timestamps
word_timestamps = output[0].timestamp['word']
# Access segment-level timestamps
segment_timestamps = output[0].timestamp['segment']
# Access character-level timestamps
char_timestamps = output[0].timestamp['char']
# Print segment timestamps
for stamp in segment_timestamps:
print(f"{stamp['start']}s - {stamp['end']}s : {stamp['segment']}")
You can also create a simple web interface for transcription using Gradio:
import gradio as gr
import nemo.collections.asr as nemo_asr
# Load model once
asr_model = nemo_asr.models.ASRModel.from_pretrained(model_name="nvidia/parakeet-tdt-0.6b-v2")
def transcribe_audio(audio, timestamps=False):
if audio is None:
return "Please upload a valid audio file."
output = asr_model.transcribe([audio], timestamps=timestamps)
if timestamps:
segments = output[0].timestamp['segment']
result = ""
for seg in segments:
result += f"{seg['start']}s - {seg['end']}s: {seg['segment']}\n"
return result
else:
return output[0].text
# Build the UI
with gr.Blocks() as demo:
gr.Markdown("# Parakeet-TDT-0.6B Speech to Text Demo")
with gr.Row():
audio_input = gr.Audio(type="filepath", label="Upload Audio (16kHz .wav preferred)")
with gr.Row():
timestamp_checkbox = gr.Checkbox(label="Enable Timestamps", value=False)
with gr.Row():
output_text = gr.Textbox(label="Transcription Output", lines=10)
submit_btn = gr.Button("Transcribe")
submit_btn.click(fn=transcribe_audio, inputs=[audio_input, timestamp_checkbox], outputs=output_text)
# Launch the UI
demo.launch(server_name="0.0.0.0", server_port=7860)
For those who want to run the model locally, here are the key requirements:
Install Python and dependencies:
sudo apt update
sudo apt install -y build-essential git wget curl ffmpeg
sudo apt install -y python3-pip
python3 -m venv nemo_env
source nemo_env/bin/activate
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu11
Install NeMo toolkit with ASR support:
pip install -U nemo_toolkit['asr']
Test the installation with a basic transcription:
import nemo.collections.asr as nemo_asr asr_model = nemo_asr.models.ASRModel.from_pretrained(model_name="nvidia/parakeet-tdt-0.6b-v2")
# Now you're ready to use the modCopyimport nemo.collections.asr as nemo_asr asr_model = nemo_asr.models.ASRModel.from_pretrained(model_name="nvidia/parakeet-tdt-0.6b-v2")
# Now you're ready to use the model
The Parakeet-TDT-0.6B-V2 model was trained using a sophisticated approach to achieve its high performance:
The model was trained on the Granary dataset, comprising approximately 120,000 hours of English speech data:
This diverse dataset ensures robust performance across various domains, accents, and recording conditions.
To get the most out of Parakeet-TDT-0.6B-v2, consider these optimisation strategies:
2. Long audio handling:
The exceptional performance and efficiency of Parakeet-TDT-0.6B-v2 make it suitable for a wide range of applications:
While Parakeet-TDT-0.6B-v2 offers impressive capabilities, it’s important to be aware of its limitations:
3. GPU dependency: The model is optimized for NVIDIA GPUs and requires appropriate hardware for optimal performance.
The innovations demonstrated in Parakeet-TDT-0.6B-v2 point to several exciting future directions for speech recognition technology:
NVIDIA’s Parakeet-TDT-0.6B-v2 sets a new standard in speech recognition by proving that smart architecture can outperform sheer model size. With its FastConformer encoder and Token-and-Duration Transducer decoder, it delivers exceptional accuracy, speed, and efficiency-all in a compact 600-million-parameter model.
For teams building real-world applications, it offers the best of both worlds: cutting-edge transcription quality and practical deployment, without the heavy hardware demands of larger models. Backed by the open-source NeMo toolkit, it’s easy to integrate, fine-tune, and scale across use cases ranging from media and accessibility to research and enterprise tools.
As the field moves forward, Parakeet-TDT-0.6B-v2 illustrates the future of speech recognition: purpose-built models that prioritize performance, precision, and usability over size alone.
By combining the efficiency of the FastConformer architecture with the speed and accuracy benefits of the Token Duration Transducer, NVIDIA has created a truly exceptional ASR model that sets new standards for the industry while remaining accessible and practical for real-world applications.
For reference you can visit this : https://medium.com/@akshaychame2/nvidia-parakeet-tdt-0-6b-v2-a-deep-dive-into-state-of-the-art-speech-recognition-architecture-d1f0b8e61e4b
Traditional keyword search stops at words. Semantic search understands meaning.
On most Drupal sites today, a user searching for “child education rights” might only get results that contain those exact words. But what if the best article uses the phrase “legal protections for minors”? That’s where Semantic Search comes in.
Semantic Search goes beyond literal matches. It uses language models and vector databases like Milvus or Pinecone to understand context and intent, delivering results that make sense to the user. Think of how Google now understands that a search for “best laptop for travel” might prioritise battery life and weight, not just articles with the word “travel.”
By connecting Semantic Search with Drupal’s Search API module, site builders can offer smarter search experiences across government portals, large publishing sites, nonprofit knowledge hubs, or healthcare platforms—anywhere users need quick, intelligent access to information.
The Drupal AI Ecosystem's AI Search module makes this powerful Semantic Search functionality possible. As part of Drupal’s growing AI ecosystem, this module integrates seamlessly with Drupal’s existing architecture, enabling developers and site builders to harness the full potential of AI-powered search without needing to build complex solutions from scratch.
By leveraging tools like vector databases (e.g., Milvus or Pinecone) and the Search API module, the AI Search module provides an intuitive way to implement Semantic Search on your Drupal site. Whether you’re running a small website or a large enterprise platform, this module ensures that your users enjoy smarter, faster, and more relevant search experiences.
Semantic Search goes beyond simple keyword matching. It understands the meaning behind user queries and matches them with content that aligns with their intent.
For example, if a user searches for "How to bake a cake," Semantic Search doesn’t just look for pages containing those exact words; it also considers related concepts like "dessert recipes," "baking tips," or "cake ingredients."
In Drupal, Semantic Search is powered by vector embeddings, which represent content as numerical vectors in a multi-dimensional space. These embeddings are stored in vector databases like Milvus or Pinecone, enabling fast and accurate similarity searches.
To implement Semantic Search in Drupal, you can use the Search API module to connect to a vector database and index your content. Here’s how it works:
Once indexed, the vector database processes the data and creates embeddings, enabling semantic matching during user queries.
To get started with Semantic Search in Drupal, you can explore these community-driven projects:
These modules make it easier to integrate advanced AI capabilities into your Drupal site.
While traditional search engines like Apache Solr or database searches rely on keyword matching and ranking algorithms, Semantic Search offers several advantages:
Imagine you’re managing a news website with thousands of articles. One morning, your editor asks, “How many ‘breaking news’ articles do we have?”
Without a custom Drush command, you’d log into the admin UI, filter the content list, and count manually tedious process.
But with a custom command, you simply type:
drush node-count-- type=breaking_news
…and get an answer in seconds!
This is the magic of Drush: transforming time-consuming tasks into instant insights. In this blog, we’ll walk through creating and refining custom Drush commands, showing how they can become indispensable tools in your Drupal workflow.
Drush isn’t just a shortcut’s a command-line interface that lets you interact with Drupal at its core. Custom commands extend this power, letting you automate workflows, query data, and manage your site without touching a browser.
Here’s how Drush can change your workflow:
Let’s start with a concrete example: a command that counts nodes. This will illustrate the key components and decisions involved in building custom commands.
node_count_module/
├── node_count_module.info.yml
├── node_count_module.services.yml
└── src/
└── Commands/
└── NodeCountCommands.php
This structure follows Drupal’s conventions, ensuring your command is discoverable and maintainable.
# node_count_module.info.yml
name: Node Count Module
type: module
description: Provides a Drush command to count nodes
core_version_requirement: ^9 || ^10 || ^11
package: Custom
This metadata tells Drupal what your module does and which versions it supports.
# node_count_module.services.yml
services:
node_count_module.drush_commands:
class: Drupal\node_count_module\Commands\NodeCountCommands
arguments:
- '@entity_type.manager'
tags:
- { name: drush.command }
Here, you’re defining a service that Drush can recognise. The @entity_type.manager argument gives your command access to Drupal’s entity system.
Learn more about services in Drupal
Let’s look at the code for a custom node-count command, with full validation and helpful output.
<?php
namespace Drupal\node_count_module\Commands;
use Drush\Commands\DrushCommands;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Provides Drush commands for node counting.
*/
class NodeCountCommands extends DrushCommands {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new NodeCountCommands object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* Count the number of nodes in the Drupal site.
*
* @command custom:node-count
* @aliases node-count nc
* @option type Filter nodes by type
* @usage custom:node-count
* Display total number of nodes
* @usage custom:node-count --type=article
* Count nodes of type 'article'
*
* @param array $options
* An array of options.
*
* @return int
* The number of nodes.
*/
public function nodeCount($options = ['type' => null]) {
// Validate node type if provided.
if (!empty($options['type']) && !$this->entityTypeManager->getStorage('node_type')->load($options['type'])) {
throw new \InvalidArgumentException("Node type '{$options['type']}' does not exist.");
}
// Get the node storage.
$nodeStorage = $this->entityTypeManager->getStorage('node');
$query = $nodeStorage->getQuery()->accessCheck(true);
// Filter by node type if specified.
if (!empty($options['type'])) {
$query->condition('type', $options['type']);
}
// Execute the query and get the count.
$count = $query->count()->execute();
// Output the result.
if (!empty($options['type'])) {
$this->output()->writeln("Total nodes of type '{$options['type']}': $count");
} else {
$this->output()->writeln("Total nodes in the site: $count");
}
return $count;
}
}
This command uses Drupal’s entity query system to count nodes, varying the result based on the --type option. The entity_type.manager service provides access to this system, making the command reusable and testable.
Official Drush Command Authoring Docs
You don’t always need a new module. Adding commands to existing ones keeps your codebase clean.
services:
your_existing_module.drush_commands:
class: Drupal\your_existing_module\Commands\NodeCountCommands
arguments:
- '@entity_type.manager'
tags:
- { name: drush.command }
This approach avoids bloating your project with unnecessary modules while keeping code organised.
As Drupal evolves, so should your commands. Here’s how you can update the same command for Drupal 10/11 using modern PHP features.
<?php
namespace Drupal\your_module\Commands;
use Drush\Commands\DrushCommands;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides Drush commands for node counting.
*/
class NodeCountCommands extends DrushCommands {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager
) {}
public static function create(ContainerInterface $container): static {
return new static(
$container->get('entity_type.manager')
);
}
/**
* Count the number of nodes in the Drupal site.
*
* @command custom:node-count
* @aliases node-count nc
* @option type Filter nodes by type
* @usage custom:node-count
* Display total number of nodes
* @usage custom:node-count --type=article
* Count nodes of type 'article'
*/
public function nodeCount($options = ['type' => null]) {
if (!empty($options['type']) && !$this->entityTypeManager->getStorage('node_type')->load($options['type'])) {
throw new \InvalidArgumentException("Node type '{$options['type']}' does not exist.");
}
$nodeStorage = $this->entityTypeManager->getStorage('node');
$query = $nodeStorage->getQuery()->accessCheck(true);
if (!empty($options['type'])) {
$query->condition('type', $options['type']);
}
$count = $query->count()->execute();
if (!empty($options['type'])) {
$this->output()->writeln("Total nodes of type '{$options['type']}': $count");
} else {
$this->output()->writeln("Total nodes in the site: $count");
}
return $count;
}
}
These changes reflect Drupal’s shift toward stricter type safety and modern PHP practices.
A content agency migrates 50,000 articles from a legacy CMS. They need to verify data integrity:
/**
* Provides Drush commands for content auditing.
*/
class ContentAuditCommands extends DrushCommands {
protected $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* Audit taxonomy terms in a vocabulary.
*
* @command custom:audit-taxonomy
* @option vocabulary The vocabulary machine name
* @usage custom:audit-taxonomy --vocabulary=tags
* Audit all terms in the 'tags' vocabulary
*/
public function auditTaxonomy($options = ['vocabulary' => 'tags']) {
$termStorage = $this->entityTypeManager->getStorage('taxonomy_term');
$query = $termStorage->getQuery()->condition('vid', $options['vocabulary']);
$termCount = $query->count()->execute();
$this->output()->writeln("Total terms in '{$options['vocabulary']}': $termCount");
return $termCount;
}
}
This command leverages Drupal’s entity system to audit taxonomy terms, ensuring the migration team gets immediate feedback on taxonomy field integrity.
Best practices for custom commands
1. Use dependency injection
Injecting services like entity_type.manager makes your command testable and flexible.
Learn more about Dependency Injection in Drupal
2. Provide clear descriptions
Use the @usage annotation to document how users should run the command.
3. Handle edge cases
What happens if a node type doesn’t exist? Add validation:
if (!empty($options['type']) && !$this->entityTypeManager->getStorage('node_type')->load($options['type'])) {
throw new \InvalidArgumentException("Node type '{$options['type']}' does not exist.");
}
This ensures your command fails gracefully instead of returning cryptic errors.
Forgetting to add drush.command in services.yml means Drush won’t recognise your command.
Using accessCheck(true) ensures the command respects user permissions. Skipping it might expose sensitive data.
Never hardcode services like \Drupal::entityTypeManager(). Always inject them for better testability.
Symptoms: drush list doesn’t show your command.
Diagnosis:
Symptoms: The command takes 10+ seconds to count nodes.
Fix: Add caching:
use Drupal\Core\Cache\Cache;
public function nodeCount($options = ['type' => null]) {
$cache_key = 'node_count:' . ($options['type'] ?? 'all');
$cached = \Drupal::cache()->get($cache_key);
if ($cached) {
$this->output()->writeln("Cached count: {$cached->data}");
return $cached->data;
}
// Validate node type if provided.
if (!empty($options['type']) && !$this->entityTypeManager->getStorage('node_type')->load($options['type'])) {
throw new \InvalidArgumentException("Node type '{$options['type']}' does not exist.");
}
$nodeStorage = $this->entityTypeManager->getStorage('node');
$query = $nodeStorage->getQuery()->accessCheck(true);
if (!empty($options['type'])) {
$query->condition('type', $options['type']);
}
$count = $query->count()->execute();
\Drupal::cache()->set($cache_key, $count, Cache::PERMANENT, ['node']);
if (!empty($options['type'])) {
$this->output()->writeln("Total nodes of type '{$options['type']}': $count");
} else {
$this->output()->writeln("Total nodes in the site: $count");
}
return $count;
}
This adds caching to prevent redundant queries during frequent audits.
Custom commands aren’t limited to counting nodes. Let’s explore advanced use cases:
A real estate platform uses a command to update property listings when market data changes:
public function updatePropertyPrices($options = ['increase' => 5]) {
$query = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', 'property');
$nids = $query->execute();
foreach ($this->entityTypeManager->getStorage('node')->loadMultiple($nids) as $node) {
$price = $node->get('field_price')->value * (1 + ($options['increase'] / 100));
$node->set('field_price', $price)->save();
}
$this->output()->writeln("Increased prices by {$options['increase']}% for " . count($nids) . " properties.");
}
A healthcare site runs nightly health checks to ensure critical content is up-to-date:
public function checkContentHealth() {
$query = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('status', 1)
->condition('changed', strtotime('-1 year'), '<');
$staleNodes = $query->execute();
if (empty($staleNodes)) {
$this->output()->writeln("All content is up-to-date!");
} else {
$this->output()->writeln("Found " . count($staleNodes) . " stale nodes. Run `drush node-check stale` for details.");
}
}
Instead of custom:node-count, use nc for brevity.
Always include @usage annotations. For instance:
/**
* @usage custom:node-count --type=article
* Count all articles
*/
Describe what the command does in plain language:
Describe what the command does in plain language:
/**
* Count nodes with optional filtering by type.
*
* This command helps you quickly audit content volume across content types.
*/
Custom Drush commands, when thoughtfully built, do more than automate tasks—they shape a cleaner, faster, and more predictable development workflow.
By applying best practices like dependency injection, validation, and clear documentation, developers create commands that are easy to maintain and trusted across teams.
Over time, these tools become integral to managing complexity, improving deployment confidence, and making Drupal development feel less like firefighting and more like engineering.
Ready to go further? Explore the official Drush command authoring docs and Drupal’s services and dependency injection guide to keep levelling up your CLI skills!
Happy Drushing!
Caching is both an art and a science. Mastering it can transform your Drupal site from a sluggish performer to a lightning-fast powerhouse.
In this final blog of our caching series, we’ll tie together everything you’ve learned: comprehensive best practices, performance monitoring, and real-world strategies.
Let’s consider what’s possible. Imagine a content-heavy education portal where students and teachers navigate through hundreds of resources daily. Without proper caching, every page request triggers a full rebuild, repeated database queries, heavy backend processing, and wasted server resources.
Now picture this: a regional school district site serving thousands of users every day. By implementing intelligent caching layers, Drupal page caching, dynamic route caching, and reverse proxies like Varnish, they cut average page load times from over 4 seconds to under 800 milliseconds. Traffic spikes from new semester enrollments no longer crash the site. Teachers upload assignments without delay. Students access learning materials instantly.
That’s the kind of transformation caching brings. By tailoring strategies to how your content is structured and consumed, you’re not just speeding up your site, you’re creating an experience that scales with demand and stays fast when it matters most.
Ready to build a caching system that evolves with your site’s needs? Let’s dive in.
Caching isn’t one-size-fits-all. Think of it as assembling a toolkit: you need the right tools for the right jobs. Let’s break down the essentials:
Your cache backend is the foundation. Here’s how to choose:
Example:
A news site might use Redis for the cache. render bin (frequent updates) and APCu for cache.dynamic_page_cache (fast access for authenticated users).
Imagine you’re updating just one article-wouldn’t it be wasteful to clear the cache for every article? By using granular cache tags like node:123, you ensure only the relevant cache is cleared. This keeps your site fast, even during bulk updates.
What are cache tags? Here’s a quick primer.
Think of invalidation as surgery, not demolition. For example, when a product price changes:
This ensures updates propagate instantly-without slowing down unrelated content.
Monitoring isn’t just about collecting numbers-it’s about understanding your site’s health. Here are four key indicators you should track:
The percentage of requests served from cache. A 95%+ hit rate means your caching is efficient.
protected function calculateCacheHitRatio() {
$cache_hits = \Drupal::state()->get('cache_performance.hits', 0);
$cache_misses = \Drupal::state()->get('cache_performance.misses', 0);
$total = $cache_hits + $cache_misses;
return $total ? round(($cache_hits / $total) * 100, 2) : 0;
}
Note: You’ll need to instrument your code to increment these counters.
How long does it take to rebuild a cache when there’s a miss? If this exceeds 500ms, investigate slow field processing or external API calls.
protected function getAverageCacheGenerationTime() {
$times = \Drupal::state()->get('cache_performance.generation_times', []);
return count($times) ? array_sum($times) / count($times) : 0;
}
Again, ensure you’re logging these times when caches are regenerated.
Track memory consumption to avoid resource exhaustion.
protected function getCacheMemoryUsage() {
return memory_get_usage(true);
}
How often are caches cleared? Frequent invalidations (e.g., hourly) may indicate over-invalidation.
protected function getInvalidationFrequency() {
return \Drupal::state()->get('cache_performance.invalidations', 0);
}
Real-World Example:
Here’s how the education portal can improve after optimization:
Drupal’s default caching is robust, but if you’re building a site that needs to scale, think global e-commerce or high-traffic news, you’ll need to go beyond the basics. Imagine a global online retailer handling millions of visits per day. By layering caches, using targeted invalidation, and preventing cache stampedes, they can see a server load drop by 85% and response times can improve by 92%.
Ready to learn how? Let’s break down these advanced techniques and see how you can apply them to your own Drupal projects.
Think of caching like a library: the most popular books are on the front shelf (memory), less popular ones are in the stacks (APCu), and the rarest are in the basement (database). Chained cache backends mimic these data flows through multiple layers, so you get speed and resilience.
Note: Drupal core doesn’t provide a built-in chained cache backend. The following is a conceptual example of how you could implement this pattern as a custom service. For more on cache backends, see the Cache Backend API docs.
use Drupal\Core\Cache\CacheBackendInterface;
class ChainedCacheBackend implements CacheBackendInterface {
protected $backends = [];
public function __construct(array $backends) {
$this->backends = $backends;
}
public function get($cid, $allow_invalid = FALSE) {
foreach ($this->backends as $i => $backend) {
$cached = $backend->get($cid, $allow_invalid);
if ($cached) {
// Populate only the faster layers above this one.
for ($j = 0; $j < $i; $j++) {
$this->backends[$j]->set($cid, $cached->data, $cached->expire, $cached->tags ?? []);
}
return $cached;
}
}
return FALSE;
}
// Implement other required methods: set(), delete(), invalidate(), etc.
}
A store with 100,000 products can use this approach:
When a user requests a product:
This keeps frequent visitors happy with lightning-fast responses, while new queries are handled efficiently.
Imagine two chefs sharing a pantry: one cooks Thai, the other Italian. If they don’t separate their ingredients, chaos ensues! The same goes for caches-unrelated data should live in separate bins to prevent accidental overwrites.
In Drupal, you usually isolate cache bins by configuring them in settings.php or via service overrides-not by prefixing cache IDS in code.
$settings['cache']['bins']['product_search'] = 'cache.backend.redis';
$settings['cache']['bins']['user_profile'] = 'cache.backend.memory';
A news site might use cache.bin.product_search for search results and cache.bin.user_profile for user data. This way, a search cache clear won’t accidentally wipe out user sessions.
Invalidation isn’t just about clearing old data-it’s about knowing what to clear and when. For example, if a content editor updates a product, you want to invalidate not just that product’s cache, but also related categories and recommendations.
Learn more: Cache Tags
class SmartCacheInvalidator {
public function invalidateRelatedContent($entity) {
$tags = [
'entity:' . $entity->getEntityTypeId(),
'entity_bundle:' . $entity->bundle(),
'entity_list',
];
\Drupal::cache()->invalidateTags($tags);
$this->triggerCustomInvalidation($entity);
}
protected function triggerCustomInvalidation($entity) {
if ($entity->hasField('related_content')) {
$related_items = $entity->get('related_content')->referencedEntities();
foreach ($related_items as $related_item) {
\Drupal::cache()->invalidateTags([
'entity:' . $related_item->getEntityTypeId() . ':' . $related_item->id(),
]);
}
}
}
}
When an article is updated:
This ensures updates propagate instantly-without over-clearing other content.
A cache stampede is like hundreds of people rushing to rebuild a lost sandcastle at once-total chaos! In Drupal, the Lock API helps by letting only one request rebuild the cache, while others wait.
use Drupal\Core\Cache\CacheBackendInterface;
class CacheStampedePreventor {
public function getCachedData($cache_id, callable $callback, array $options = []) {
$lock_id = "cache_rebuild:{$cache_id}";
$cached = \Drupal::cache()->get($cache_id);
if ($cached) {
return $cached->data;
}
$lock = \Drupal::lock();
if ($lock->acquire($lock_id)) {
try {
$data = $callback();
\Drupal::cache()->set(
$cache_id,
$data,
CacheBackendInterface::CACHE_PERMANENT,
$options['tags'] ?? []
);
$lock->release($lock_id);
return $data;
} catch (\Exception $e) {
$lock->release($lock_id);
throw $e;
}
} else {
// Wait for the other process to finish rebuilding.
if (method_exists($lock, 'wait')) {
$lock->wait($lock_id);
} else {
// Fallback: sleep briefly before retrying.
sleep(1);
}
$cached = \Drupal::cache()->get($cache_id);
return $cached ? $cached->data : null;
}
}
}
Note: The wait() method is available in most lock backends in Drupal 9/10, but always check your environment.
During a flash sale, thousands hit the same product page. The first request rebuilds the cache; everyone else waits for their turn, no server meltdown!
Cache contexts tell Drupal when to vary the cache (e.g., by user role or language). But what if you need to vary by device type, or a combination of factors? That’s where custom cache contexts shine.
Learn more: Cache Contexts
use Drupal\Core\Cache\CacheContextInterface;
class DynamicCacheContext implements CacheContextInterface {
public function getContext() {
$current_user = \Drupal::currentUser();
return md5(serialize([
'user_roles' => $current_user->getRoles(),
'device_type' => $this->getDeviceType(),
]));
}
protected function getDeviceType() {
$user_agent = \Drupal::request()->headers->get('User-Agent');
return preg_match('/mobile/i', $user_agent) ? 'mobile' : 'desktop';
}
public static function getLabel() {
return t('User roles and device type');
}
}
A news app serves different layouts to mobile and desktop users. With a dynamic context:
Monitoring isn’t just about pretty graphs-it’s about actionable insights. You want to know:
Learn more: Logger API
class CachePerformanceTracker {
public function trackCachePerformance(callable $callback) {
$start_time = microtime(true);
$memory_before = memory_get_usage();
$result = $callback();
$execution_time = microtime(true) - $start_time;
\Drupal::logger('cache_performance')->info(
'Execution Time: @time ms, Memory Used: @memory KB',
[
'@time' => round($execution_time * 1000, 2),
'@memory' => round((memory_get_usage() - $memory_before) / 1024, 2),
]
);
return $result;
}
}
A dashboard tracks:
This helps engineers quickly spot and fix bottlenecks-like a slow block dragging down the homepage.
1. Layered Caching architecture
Use chained backends for critical paths (e.g., product pages), and isolated bins for unrelated systems (e.g., user sessions vs. search results).
2. Contextual Caching
Be specific: avoid broad contexts like url; use url.path or custom contexts for business logic.
3. Granular Invalidation
Target specific entities (node:123) instead of broad tags (content_type:article).
4. Stampede Protection
Apply cache locks to high-traffic pages with frequent updates (e.g., stock tickers, live scores).
5. Continuous Monitoring
Log cache performance and set up alerts for anomalies (like a sudden drop in hit rate).
This article is part of our comprehensive 10-part series on Drupal caching:
Advanced caching techniques help Drupal sites stay responsive under pressure and efficient at scale. By layering cache backends, isolating bins, using precise invalidation, and adapting to context, you create a caching system that mirrors how real users interact with your site.
These methods work best when applied as part of a clear strategy. Each layer supports faster access. Each boundary between cache bins keeps systems focused. Context-aware caching and stampede prevention protect performance without adding complexity. And monitoring ensures every choice is informed by real data.
Together, they form a foundation for stability and long-term maintainability.
In our final blog, we’ll tie everything together with Drupal caching best practices. You’ll learn how to:
Stay tuned for the final chapter in our caching journey!
Drupal’s default cache bins-such as cache. render and cache. Entities are designed for generalised use cases.
However, complex applications often require specialised caching strategies that transcend these built-in systems.
Imagine a scientific research platform that processes large datasets for computational models.
Default caching might reduce database queries, but it fails to address the computational overhead of recalculating results. In such cases, custom cache bins become essential to balance performance with precision.
Default cache bins are optimised for simplicity, but they struggle with niche requirements:
Custom bins shine in scenarios where default bins fall short:
Tip: Avoid custom bins for rapidly changing data (like live chat messages) or sensitive information (such as user-specific financial records), where default mechanisms or no caching may be safer.
Think of cache bins as labelled storage boxes in a warehouse. Each box holds a different type of item, and you can choose where to store each box for maximum efficiency. In Drupal, cache bins:
Drupal ships with predefined bins for common use cases:
While these bins handle most scenarios, they lack flexibility for specialised needs.
Why create custom Cache bins?
A custom cache bin relies on four foundational elements:
Drupal’s service container allows cache logic to be decoupled from business logic. By injecting dependencies like CacheBackendInterface, services become:
Example:
use Drupal\Core\Cache\CacheBackendInterface;
class SpecializedCacheBin implements CacheBackendInterface {
protected $cacheBin;
public function __construct(CacheBackendInterface $cache_bin) {
$this->cacheBin = $cache_bin;
}
public function get($cid, $allow_invalid = FALSE) {
return $this->cacheBin->get($cid, $allow_invalid);
}
// Implement all other required CacheBackendInterface methods...
}
Official Documentation: Service Container
Define the bin in your module’s mymodule.services.yml:
services:
cache.specialized_bin:
class: Drupal\mymodule\Cache\SpecializedCacheBin
arguments: ['@cache_factory']
tags:
- { name: cache.bin }
Note: The cache backend (e.g., Redis, database) is configured in settings.php, not in the service definition.
$settings['cache']['bins']['specialized_bin'] = 'cache.backend.redis'; // or 'cache.backend.database'
Here’s an example for a scientific computation cache:
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheFactoryInterface;
class ScientificComputationCache implements CacheBackendInterface {
protected $cacheBin;
public function __construct(CacheFactoryInterface $cache_factory) {
$this->cacheBin = $cache_factory->get('scientific_computation');
}
public function get($cid, $allow_invalid = FALSE) {
$cached = $this->cacheBin->get($cid, $allow_invalid);
if ($cached && $this->validateScientificData($cached->data)) {
return $cached;
}
return FALSE;
}
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = []) {
$processed_data = $this->preprocessData($data);
$this->cacheBin->set(
$cid,
$processed_data,
$expire,
array_merge($tags, ['scientific_computation'])
);
}
protected function validateScientificData($data) {
return is_array($data) && isset($data['result']) && isset($data['timestamp']);
}
protected function preprocessData($data) {
return [
'result' => $data,
'timestamp' => time(),
'source_hash' => md5(serialize($data)),
];
}
// Implement all other required CacheBackendInterface methods...
}
Scenario: A recommendation engine generates predictions that take 5 seconds to compute.
Solution: Cache predictions with Redis for fast retrieval.
use Drupal\Core\Cache\CacheBackendInterface;
class MachineLearningModelCache {
protected $cache;
public function __construct(CacheBackendInterface $cache) {
$this->cache = $cache;
}
public function cacheModelPrediction($model_id, $input, $prediction) {
$cache_id = 'ml_prediction:' . md5($model_id . serialize($input));
$this->cache->set(
$cache_id,
[
'model' => $model_id,
'input' => $input,
'prediction' => $prediction,
'timestamp' => time(),
],
CacheBackendInterface::CACHE_PERMANENT,
['machine_learning_predictions']
);
}
}
Impact:
Scenario: A mapping application serves pre-rendered tiles that take 2 seconds to generate.
Solution: Use a file-based bin to offload the database.
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\File\FileSystemInterface;
class MapTileCache implements CacheBackendInterface {
protected $cacheBin;
protected $fileSystem;
public function __construct(CacheBackendInterface $cache_bin, FileSystemInterface $file_system) {
$this->cacheBin = $cache_bin;
$this->fileSystem = $file_system;
}
public function set($cid, $data, $expire, array $tags) {
$uri = "public://cache/tiles/$cid.png";
$this->fileSystem->saveData($data, $uri, FileSystemInterface::EXISTS_REPLACE);
$this->cacheBin->set($cid, ['uri' => $uri], $expire, $tags);
}
public function get($cid, $allow_invalid = FALSE) {
$cache = $this->cacheBin->get($cid, $allow_invalid);
if ($cache && isset($cache->data['uri']) && file_exists($cache->data['uri'])) {
return file_get_contents($cache->data['uri']);
}
return FALSE;
}
// Implement all other required CacheBackendInterface methods...
}
Impact:
Track cache activity for auditing and debugging:
class CacheBinDebugger {
public static function logCacheOperation($bin, $operation, $cid) {
\Drupal::logger('cache_debug')->info(
'Cache Operation: @operation in @bin for @cid',
[
'@operation' => $operation,
'@bin' => $bin,
'@cid' => $cid,
]
);
}
}
Why it works:
Use backend-specific tools to measure effectiveness:
function mymodule_monitor_cache_bin_performance($bin_name) {
$cache = \Drupal::cache($bin_name);
// Example: Redis-specific stats (if using Redis)
if (method_exists($cache, 'getConnection')) {
$stats = $cache->getConnection()->info();
\Drupal::logger('cache_performance')->info(
'Redis Stats for @bin: @stats',
[
'@bin' => $bin_name,
'@stats' => print_r($stats, TRUE),
]
);
}
}
Metrics to monitor:
This article is part of a comprehensive 10-part series on Drupal caching:
As we wrap up, remember that implementing custom cache bins in Drupal is surprisingly straightforward once you understand the core concepts. Define your service in your module's services.yml file, create your cache bin class extending the appropriate backend, and register everything properly. These three steps will get you most of the way there. The real power comes when you fine tune your bin's configuration to match your specific caching needs and invalidation patterns. Don't hesitate to experiment with different backends and expiration strategies. Sometimes the best implementation reveals itself through trial and optimization. Your site's performance metrics will tell the story of your success.
Next, we’ll look at how advanced caching techniques build on this foundation. We’ll walk through cache hierarchies (memory → Redis → database), event-driven invalidation, and integrating with CDNs or edge networks to extend performance beyond the application layer.
Drupal’s default caching mechanisms include cache.render and cache.page are designed for general use cases. They work well for standard content delivery, reducing load times and database queries across a broad range of applications. However, complex applications often require specialised caching logic that transcends these built-in systems.
Take, for instance, a financial reporting platform that processes thousands of data points per request. Default caching might reduce some backend calls, but it doesn’t address the computational overhead of transforming raw financial data into user-specific reports with real-time accuracy. Similarly, consider a personalised learning platform that dynamically assembles course content based on a learner’s history, progress, and preferences. In both cases, caching needs to go beyond what Drupal offers out of the box.
Default strategies fall short when performance demands collide with precision and dynamic output. In such scenarios, a custom caching service becomes essential, engineered to recognise context-specific data dependencies, invalidate intelligently, and serve pre-processed results without compromising on accuracy or personalisation.
Default caching systems are optimised for simplicity, but they struggle with niche requirements:
Custom services shine in scenarios where default caching falls short:
Avoid custom caching for rapidly changing data (like live chat messages) or sensitive information (such as user-specific financial records), where default mechanisms or no caching at all may be safer.
A custom caching service in Drupal relies on four foundational elements:
1. Cache backend
This defines where data is stored. Common backends include:
2. Cache tags
Tags determine what triggers cache invalidation. For example:
Avoid broad tags like content_type: article, which can inadvertently clear unrelated caches.
3. Cache contexts
Contexts define when to vary the cache. For instance:
Use narrow contexts (like url. path instead of url) to prevent cache bloat. Note: For custom cache bins, you must include context values in your cache ID, as cache contexts are not natively supported outside render caching.
4. Cache metadata
Metadata includes rules like max-age (how long to store data) and dependencies (e.g., tags, contexts). This metadata ensures cached data remains fresh and relevant.
Dependency injection: why it matters
Drupal’s service container allows caching logic to be decoupled from business logic. By injecting dependencies like CacheBackendInterface and CacheContextsManager, services become:
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\Context\CacheContextsManager;
class CustomCacheService {
protected $cacheBackend;
protected $cacheContextsManager;
public function __construct(
CacheBackendInterface $cache_backend,
CacheContextsManager $cache_contexts_manager
) {
$this->cacheBackend = $cache_backend;
$this->cacheContextsManager = $cache_contexts_manager;
}
}
Learn about dependency injection in Drupal.
Define the service in mymodule.services.yml:
services:
mymodule.custom_cache:
class: Drupal\mymodule\Cache\CustomCacheService
arguments:
- '@cache.default'
- '@cache.contexts_manager'
This binds the service to Drupal’s dependency injection system, enabling reusability and testability.
Create a method to fetch and cache data. Note: For custom cache bins, you must handle cache contexts by including them in the cache ID if needed.
public function getCachedData($cache_id, $callback, $options = []) {
$options += [
'expire' => \Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT,
'tags' => ['my_custom_cache'],
];
if ($cached = $this->cacheBackend->get($cache_id)) {
return $cached->data;
}
$data = $callback();
$this->cacheBackend->set(
$cache_id,
$data,
$options['expire'],
$options['tags']
);
return $data;
}
How It works:
See the official Cache API usage guide.
Multi-level caching balances speed and resilience by combining storage tiers:
class MultiLevelCacheService {
protected $memoryCache = [];
protected $localCache;
protected $distributedCache;
public function __construct($localCache, $distributedCache) {
$this->localCache = $localCache;
$this->distributedCache = $distributedCache;
}
public function getData($key, $fetchCallback) {
if (isset($this->memoryCache[$key])) {
return $this->memoryCache[$key];
}
if ($local_cached = $this->localCache->get($key)) {
$this->memoryCache[$key] = $local_cached->data;
return $local_cached->data;
}
if ($distributed_cached = $this->distributedCache->get($key)) {
$this->localCache->set($key, $distributed_cached->data, \Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT);
$this->memoryCache[$key] = $distributed_cached->data;
return $distributed_cached->data;
}
$data = $fetchCallback();
$this->memoryCache[$key] = $data;
$this->localCache->set($key, $data, \Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT);
$this->distributedCache->set($key, $data, \Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT);
return $data;
}
}
Use case:
A news site with high traffic on its home page. The memory cache handles immediate requests, while the distributed layer ensures consistency across a server cluster.
Scenario
An e-commerce platform pulls product pricing from a third-party API. Without caching, each request takes 2 seconds due to network latency.
Solution
Add a caching layer to reduce latency:
class ApiCacheService extends CustomCacheService {
protected $apiClient;
public function __construct($cache_backend, $cache_contexts_manager, $apiClient) {
parent::__construct($cache_backend, $cache_contexts_manager);
$this->apiClient = $apiClient;
}
public function fetchWithCaching($endpoint, $params = []) {
$cache_id = 'api_cache:' . md5($endpoint . serialize($params));
return $this->getCachedData($cache_id, function() use ($endpoint, $params) {
try {
$response = $this->apiClient->request($endpoint, $params);
return $this->processApiResponse($response);
} catch (\Exception $e) {
\Drupal::logger('api_cache')->error('API fetch failed: @message', ['@message' => $e->getMessage()]);
return null;
}
}, [
'expire' => REQUEST_TIME + 1800,
'tags' => ['external_api_data'],
]);
}
protected function processApiResponse($response) {
// Transform or sanitise as needed.
return $response;
}
}
Impact:
When a cache backend fails (e.g., Redis crashes), a fallback mechanism prevents downtime.
public function getFallbackData($primary_key, $fallback_callback) {
try {
return $this->getCachedData($primary_key, $fallback_callback);
} catch (\Exception $e) {
\Drupal::logger('cache_service')->warning('Primary cache failed, using fallback');
return $fallback_callback();
}
}
Why it works:
Create a tracker to measure cache effectiveness:
class CachePerformanceTracker {
protected $stats = ['hits' => 0, 'misses' => 0, 'generation_time' => []];
public function trackCachePerformance($cache_id, $callback) {
$start = microtime(true);
$result = $callback();
$this->stats['generation_time'][$cache_id] = microtime(true) - $start;
return $result;
}
Metrics to monitor:
This article is part of our comprehensive 10-part series on Drupal caching:
While Drupal’s default caching covers the basics, scaling performance for advanced applications often means thinking beyond the core. Custom caching services allow developers to fine-tune how data is stored, served, and invalidated, especially when dealing with highly dynamic or resource-intensive workloads.
In the next blog, we’ll explore how to go one step further with custom cache bins. You’ll learn how to create isolated cache storage for niche scenarios like session-based data, configure these bins in settings.php, and fine-tune caching strategies for performance-critical use cases.
To dive deeper into Drupal’s caching capabilities in the meantime, check out the Cache API documentation, along with resources on Cache Tags, Cache Contexts, and Cacheable Metadata.

Drupal’s entity system is powerful, but without proper caching, it can become a performance bottleneck. Understanding entity and render caching is crucial for building high-performance Drupal websites that can handle complex content structures efficiently.
In this blog, we’ll uncover the strategies that made this transformation possible.
Drupal’s entity caching system is a multi-layered mechanism that optimises performance by reducing redundant database queries and PHP processing. To understand its depth, let’s break down each layer and explore how it works under the hood.
What it does
When an entity (e.g., a node, user, or taxonomy term) is requested, Drupal typically queries the database to load its raw data. Entity Load Caching stores this raw data in the cache_entity bin (by default) to avoid repeated database calls for the same entity. The cache key is typically in the format entity:{entity_type}:{id}:{langcode} (e.g., entity:node:123:en).
Technical mechanics
Why It matters
What it does
After an entity is loaded, Drupal generates a render array (a structured PHP array defining how the entity should be displayed). Render Caching stores these arrays to avoid rebuilding them on every request.
Technical mechanics
Why it matters
Example:
// Add cache metadata to a render array.
$build['my_element'] = [
'#markup' => 'Cached Content',
'#cache' => [
'contexts' => ['user.roles'],
'tags' => ['node:123'],
'max-age' => 3600,
],
];
What it does
Entities are often displayed in different formats (e.g., “teaser” for listings and “full” for detail pages). View Mode Caching stores separate render arrays for each view mode to avoid redundant processing.
Technical mechanics
Why it matters
Example hook:
function mymodule_node_view(array &$build, \Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display, $view_mode) {
if ($view_mode === 'teaser') {
$build['#cache']['max-age'] = 7200;
}
}
What it does
Fields (e.g., a “body” text field or an “image” field) can be cached individually to avoid reprocessing static content.
Technical mechanics
Why it matters
Example hook:
function mymodule_field_formatter_view_alter(array &$element, array $context) {
if ($context['field_name'] === 'field_price') {
$element['#cache'] = [
'contexts' => ['user.roles'],
'tags' => ['field:price'],
'max-age' => 1800,
];
}
}
Scenario
An e-commerce site needs fast delivery of product pages for anonymous users but must update instantly when inventory changes.
Goal
Combine entity load caching with tag-based invalidation to reduce database queries while keeping content fresh.
Sample implementation
/**
* Implements hook_node_view().
*
* Adds cache metadata for product nodes with tag-based invalidation.
*/
function mymodule_node_view(array &$build, \Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display, $view_mode) {
if ($entity->bundle() === 'product' && $view_mode === 'full') {
$build['#cache']['tags'][] = 'product_list';
$build['#cache']['max-age'] = 3600;
}
}
Explanation:
This caches product entities for 1 hour (max-age) but clears the cache when the entity is edited (node:$nid) or when the product list changes (product_list).
The render pipeline is the sequence of steps Drupal uses to transform entities into HTML. Let’s dissect each stage and explore its role in caching.
What happens
Caching interaction
What happens
Caching interaction
What happens
Caching interaction
What happens
Technical details
Example:
// Bubbling metadata from a child element to a parent.
$parent['child_element'] = [
'#markup' => 'Cached Child',
'#cache' => [
'contexts' => ['user.roles'],
'tags' => ['node:123'],
'max-age' => 3600,
],
];
$parent['#cache'] = [
'contexts' => ['url.path'],
'tags' => ['page:home'],
'max-age' => 7200,
];
// After bubbling, the parent's metadata includes:
// contexts = ['url.path', 'user.roles']
// tags = ['page:home', 'node:123']
// max-age = 3600 (the lower of 7200 and 3600).What happens
Caching interaction
Performance impact
Scenario
A news site’s article pages take 2+ seconds to load. Profiling shows repeated field processing.
Goal
Add cache metadata to preprocess functions to reduce redundant processing.
Sample implementation
* Implements hook_preprocess_node().
*
* Adds cache metadata to node templates.
*/
function mymodule_preprocess_node(array &$variables) {
$node = $variables['node'];
$variables['#cache'] = [
'contexts' => ['user.roles', 'language'],
'tags' => ['node:' . $node->id(), 'content_type:article'],
'max-age' => 1800,
];
}
Explanation:
This caches the rendered HTML for 30 minutes, varying by user role and language. Tags ensure the cache clears when the article is updated.
Drupal’s caching system relies on cache contexts and cache tags to balance performance with precision. These mechanisms ensure cached content is both fast and correct, avoiding stale data while minimizing redundant computation. Let’s dissect their mechanics, interactions, and real-world implications.
What they do
Cache contexts define when cached content should differ. They act as variation keys—if any context value changes, Drupal generates a new cache entry.
Technical mechanics
Why it matters
Example:
// Cache a block differently for editors vs. anonymous users.
$build['my_block'] = [
'#markup' => 'Welcome, ' . \Drupal::currentUser()->getAccountName(),
'#cache' => [
'contexts' => ['user.roles'],
'tags' => ['user:' . \Drupal::currentUser()->id()],
'max-age' => 900,
],
];
What they do
Cache tags define what triggers cache invalidation. They tie cached data to entities or external dependencies (e.g., node:123, config:system.site). When a tag is invalidated, all cache entries referencing it are cleared.
Technical mechanics
Why it matters
Example:
// Invalidate a cached view when any article is updated.
$build['my_view'] = [
'#markup' => $rendered_view,
'#cache' => [
'tags' => ['content_type:article'],
'max-age' => 3600,
],
];
How they work together
Example:
'#type' => 'form',
'#form' => '\Drupal\comment\Form\CommentForm',
'#cache' => [
'contexts' => ['user', 'session'], // Varies for logged-in users.
'tags' => ['node:123'], // Invalidates when the node changes.
'max-age' => 0, // Never cached for anonymous users.
],
];
Scenario
A multilingual site serves French and English articles. Without proper caching, users occasionally see mixed-language content.
Goal
Cache article pages by language to prevent mixed outputs.
Sample implementation
/**
* Implements hook_entity_view_alter().
*
* Ensures multilingual content is cached per language.
*/
function mymodule_entity_view_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, $view_mode) {
// Initialize cache metadata if not set.
if (!isset($build['#cache'])) {
$build['#cache'] = [];
}
// Merge cache contexts.
$build['#cache']['contexts'] = isset($build['#cache']['contexts'])
? CacheableMetadata::mergeContexts($build['#cache']['contexts'], ['languages:language_interface'])
: ['languages:language_interface'];
// Merge cache tags.
$node_tag = 'node:' . $entity->id();
$language_tag = 'language:' . $entity->language()->getId();
$build['#cache']['tags'] = isset($build['#cache']['tags'])
? CacheableMetadata::mergeTags($build['#cache']['tags'], [$node_tag, $language_tag])
: [$node_tag, $language_tag];
// Optionally set max-age (default permanent).
if (!isset($build['#cache']['max-age'])) {
$build['#cache']['max-age'] = CacheableMetadata::CACHE_PERMANENT;
}
}
Explanation:
This ensures the cache varies by language, preventing French users from seeing English content and vice versa.
Field-level caching optimises performance by storing the rendered output of individual fields (e.g., text, images, computed values) independently. This allows static fields to be cached for extended periods while dynamic fields remain fresh. Unlike entity-level caching, field-level caching operates at a granularity that avoids reprocessing expensive formatters (e.g., geolocation maps, computed prices) unnecessarily.
A. field formatters and render arrays
Fields are rendered via formatters, which are plugins implementing FieldFormatterInterface. Each formatter generates a render array with attached cache metadata.
Example formatter flow:
// Example of a field formatter's viewElements() method.
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#markup' => $this->sanitizeValue($item->value),
'#cache' => [
'contexts' => ['user.roles'],
'tags' => ['field:custom_text'],
'max-age' => 3600,
],
];
}
return $elements;
}
B. Cache metadata bubbling
Field-level cache metadata (contexts, tags, max-age) is bubbled up to parent containers (e.g., entities, pages) during render pipeline processing. This ensures invalidation cascades correctly.
Example:
A node’s field_image with #cache['tags'] = ['node:123'] ensures the entire node’s cache clears when the image updates.
Scenario
A price field must show discounted prices to logged-in users but static pricing for guests.
Goal
Cache the field for 30 minutes, but vary by user role.
Sample implementation
/**
* Implements hook_field_formatter_view_alter().
*/
function mymodule_field_formatter_view_alter(&$element, $context) {
$element['#cache'] = [
'contexts' => ['user.roles'],
'tags' => ['field:price', 'user:' . \Drupal::currentUser()->id()],
'max-age' => 1800,
];
}
Explanation:
This creates separate cache entries for different user roles while invalidating the cache when the price field changes.
Cache bubbling is the process by which cache metadata (contexts, tags, and max-age) from deeply nested render elements (e.g., fields, blocks, forms) propagates upward to parent containers (e.g., entities, pages). This mechanism ensures that the caching behaviour of a composite element reflects all its dependencies, preventing stale content and maintaining cache coherence across the entire render tree.
Without cache bubbling, a parent container might cache independently of its children, leading to situations where an updated field or block remains invisible until the parent’s cache expires. Bubbling guarantees that invalidation cascades correctly and that cache variations (e.g., role-specific content) are preserved at every level.
A. How metadata propagates
Cache metadata flows upward through the render array hierarchy:
Example:
// A render array with a cached child element.
$parent = [
'#type' => 'container',
'#cache' => [
'contexts' => ['url.path'],
'tags' => ['page:home'],
'max-age' => 7200,
],
'child' => [
'#markup' => 'Cached Content',
'#cache' => [
'contexts' => ['user.roles'],
'tags' => ['node:123'],
'max-age' => 3600,
],
],
];
// After bubbling, the parent's metadata becomes:
$parent['#cache'] = [
'contexts' => ['url.path', 'user.roles'], // Union of contexts.
'tags' => ['page:home', 'node:123'], // Combined tags.
'max-age' => 3600, // Lowest max-age.
];
B. Bubbleable metadata interface
Drupal core uses the BubbleableMetadata class to manage metadata propagation. Developers can manually manipulate it:
use Drupal\Core\Render\BubbleableMetadata;
// Create a BubbleableMetadata object.
$metadata = BubbleableMetadata::createFromRenderArray($element);
// Merge metadata from a child element.
$metadata->merge(BubbleableMetadata::createFromRenderArray($child_element));
// Apply merged metadata back to the parent.
$metadata->applyTo($parent_element);
Example:
A node with a field_comments block will inherit the block’s comment:* tags. Updating a comment invalidates the node’s cache.
Scenario
A custom block inside an article page isn’t invalidated when the article is updated.
Goal
Log cache metadata to identify missing dependencies.
Sample implementation
function mymodule_debug_cache_bubbling($element) {
$cache_contexts = $element['#cache']['contexts'] ?? [];
$cache_tags = $element['#cache']['tags'] ?? [];
\Drupal::logger('cache_debug')->info(
'Cache Contexts: @contexts, Tags: @tags',
[
'@contexts' => implode(', ', $cache_contexts),
'@tags' => implode(', ', $cache_tags)
]
);
}
Explanation:
This logs cache metadata to help identify if the block is missing a critical tag like node:123.
Optimising caching in Drupal requires balancing speed and correctness. By applying targeted strategies, developers can reduce server load while ensuring users see up-to-date content. Below are battle-tested techniques, complete with code examples and real-world applications.
Why it matters
Broad tags like content_type:article invalidate caches unnecessarily. Granular tags (e.g., node:123, field: price) ensure only relevant content updates.
Technical implementation
Example:
/**
* Implements hook_block_view_alter().
*
* Cache a product block with entity-specific tags.
* Example assumes 'field:product_promo' is a custom tag for the promo field.
*/
function mymodule_block_view_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
if ($block->getPluginId() === 'product_promo_block') {
$product_id = \Drupal::routeMatch()->getParameter('product');
$build['#cache'] = [
'tags' => ['node:' . $product_id, 'field:product_promo'],
'max-age' => 3600,
];
}
}
Why it matters
Each context multiplies the number of cache variants. For example, URL context creates separate caches for /page vs. /page?foo=bar, even if the content doesn’t vary by query parameters.
Technical implementation
Example:
/**
* Implements hook_preprocess_block().
*
* Cache a block by path, not full URL.
*/
function mymodule_preprocess_block(&$variables) {
$variables['#cache'] = [
'contexts' => ['url.path', 'language'],
'tags' => ['block:promo_banner'],
'max-age' => 7200,
];
}
Why it matters
Not all content benefits from caching. Static fields (e.g., author bios) gain speed, while dynamic content (e.g., form validation errors) must bypass caching.
Technical implementation
Example:
/**
* Implements hook_form_alter().
*
* Prevent CSRF token caching in forms.
*/
function mymodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
if ($form_id === 'contact_form') {
$form['#cache'] = [
'contexts' => ['user'],
'max-age' => 0,
];
}
}
Why it matters
Caching session-specific or PII (Personally Identifiable Information) risks exposing private data. Always vary by user or disable caching entirely.
Technical implementation
Example:
/**
* Implements hook_preprocess_node().
*
* Prevent caching of user-specific content.
*/
function mymodule_preprocess_node(&$variables) {
$node = $variables['node'];
if ($node->getType() === 'private_document') {
$variables['#cache'] = [
'contexts' => ['user'],
'tags' => ['node:'. $node->id()],
];
}
}
Why it matters
Caching optimisations are only valuable if they reduce database load and improve page speed. Use metrics to validate changes.
Technical implementation
Example:
// Log cache hit/miss statistics via a custom service.
class CacheStatsLogger {
public function logCacheStatus($cid, $cache_hit) {
$logger = \Drupal::logger('cache_stats');
if ($cache_hit) {
$logger->info('Cache hit for @cid', ['@cid' => $cid]);
} else {
$logger->warning('Cache miss for @cid', ['@cid' => $cid]);
}
}
}
Common pitfalls and how to avoid them
Performance is often the difference between a Drupal site that users trust and one they abandon. Caching is not just a performance enhancer, it’s a core part of creating fast, reliable digital experiences. When used correctly, Drupal’s caching strategies can significantly lower page load times, reduce server strain, and improve the overall responsiveness of a site.
Real-world examples show the impact clearly. One e-commerce platform reduced page load times by 65% and server load by almost 70% after fine-tuning its caching layers. A large educational portal managing thousands of daily visitors improved homepage render times by more than 50% by configuring views and page caching strategically. These improvements didn’t just make the sites faster, they also supported higher engagement, better user retention, and lower hosting costs.
In this blog, we’ll cover the full range of Drupal caching techniques, from internal page caching to dynamic page caching, views caching, and form caching. Along with technical explanations, you’ll find real examples, common pitfalls to avoid, and direct links to Drupal’s official documentation for deeper reference.
The goal: helping you master caching, so your site feels faster, scales smarter, and stays responsive no matter how much traffic you handle.
Drupal Views is a powerful tool that lets you create dynamic lists and pages from your content. However, every time a view is executed, Drupal queries the database and rebuilds the view. An operation that can be heavy under load.
Let’s explore the different caching strategies available:
The scenario
Imagine managing a high-traffic news website. The “Latest Articles” view is accessed by thousands of users every day. Without caching, each page load forces Drupal to rebuild the list of articles, placing a heavy load on your server.
The goal
Combine time-based caching (to re-cache periodically) with tag-based caching (to immediately clear the cache when an article is updated). This ensures your users receive fast, up-to-date content.
Sample implementation
/**
* Implements hook_views_pre_build().
*
* This function configures the "latest_articles_view" with a hybrid caching strategy.
* It caches the view for 1 hour but invalidates the cache immediately if articles change.
*/
function mymodule_views_pre_build($view) {
if ($view->id() === 'latest_articles_view') {
$view->setCache([
'type' => ['time', 'tag'], // Combining time-based and tag-based caching.
'max_age' => 3600, // Cache duration: 1 hour.
'contexts' => [
'languages', // Maintain separate caches per language.
'user.permissions', // Different cache variants if permissions affect visibility.
],
'tags' => [
'article_list', // Invalidate cache when articles are updated.
],
]);
}
}
Explanation: This example demonstrates how to customize caching for the “latest_articles_view” to achieve fast response times while keeping the content fresh when articles are updated.
Query result caching stores the raw output of database queries rather than the fully rendered view. This approach targets the most resource-intensive part of data retrieval.
The scenario
Consider an online store with a “Products” view that executes heavy SQL queries involving multiple table joins. Constantly executing these queries for every request can significantly strain your database server.
The goal
Cache the raw SQL query results for a duration (e.g., 1 hour) so that repeated requests retrieve results from the cache rather than re-running the query, unless a product update occurs.
Sample implementation
/**
* Implements hook_views_pre_build().
*
* This snippet sets a caching strategy for the "my_product_view" to store raw query results.
* It reduces the need for repeated heavy queries, caching results for 1 hour.
*/
function mymodule_views_pre_build($view) {
if ($view->id() === 'my_product_view') {
$view->setCache([
'type' => ['time', 'tag'], // Utilize both time and tag-based caching.
'max_age' => 3600, // Cache lifespan: 1 hour.
'contexts' => [
'languages',
'user.permissions',
],
'tags' => [
'node_list',
'product_list' // Invalidate cache when product data updates.
],
]);
}
}
Explanation: By caching the raw output of heavy queries for “my_product_view,” the system minimises redundant database operations. Tag-based invalidation ensures that when product details change, the cache is promptly refreshed.
Rendered output caching takes the efficiency a step further by storing the complete HTML output generated by a view. This helps to bypass the resource-intensive rendering process on subsequent requests.
Documentation:
More details can be found in the Drupal Render API Documentation.
The scenario
Think of a product catalogue page where the layout is largely static despite occasional data updates. Re-rendering this static layout for every user can be inefficient.
The goal
Cache the fully rendered HTML output for 1 hour, creating variations based on user roles if needed, to serve pages swiftly without sacrificing dynamic content accuracy.
Sample implementation
/**
* Implements hook_views_post_render().
*
* This function embeds caching metadata into the final rendered output.
* The view’s complete HTML is then cached for 1 hour, and cache variants are created based on user roles.
*/
function mymodule_views_post_render($view, &$output) {
$output['#cache'] = [
'max-age' => 3600, // Cache for 1 hour.
'contexts' => ['user.roles'], // Separate caches for different user roles.
'tags' => ['content_list'], // Invalidate cache when content updates.
];
}
Explanation: This configuration ensures that once the view renders its HTML, that version is cached for quick reuse, reducing the need to run the rendering pipeline on every page load.
Forms in Drupal are dynamic interfaces that include user input, interactive elements, and security tokens (such as CSRF tokens). Caching forms is challenging because:
Documentation:
Refer to the Drupal Forms API documentation for details on managing form rendering and state.
The scenario
Imagine an e-commerce checkout form. It contains static elements like step-by-step guidance and dynamic elements like personalized promotions and shipping fields. You want to cache the static pieces for speed, but always render the dynamic sections in real time.
The goal
Implement a caching configuration that applies to the non-sensitive parts of the form, using precise cache contexts to isolate user-specific data.
Sample implementation
/**
* Implements hook_form_alter().
*
* This function applies caching directives to "my_dynamic_form".
* It ensures that while static form fragments may be cached, dynamic elements are always rendered freshly.
*/
function mymodule_form_alter(&$form, &$form_state, $form_id) {
if ($form_id === 'my_dynamic_form') {
$form['#cache'] = [
'contexts' => [
'user', // Unique cache per user.
'user.roles', // Tailor cache entries for different roles.
'url.path', // Sensitive to the current page URL.
],
'tags' => ['user:' . \Drupal::currentUser()->id()], // Unique user-specific tag.
];
}
}
Explanation: This snippet shows how to safely cache parts of a dynamic form. The use of fine-grained cache contexts ensures that cached fragments are correctly isolated per user, thus enhancing speed without compromising security.
Contextual filters allow you to tailor the output of your views using dynamic parameters (such as URL segments or user inputs). These filters benefit from caching just like any other view component. Fortunately, the default views plugins in Drupal already handle caching for contextual filters.
Note: Since the default caching for contextual filters is already handled by the Views module, there is usually no need to add extra code unless you are developing a custom filter plugin with specialised caching needs.
While caching dramatically boosts performance, it’s crucial to ensure that it doesn’t compromise your site’s security. Consider the following points:
Documentation:
Visit Drupal Security Best Practices for comprehensive guidance.
Let’s consolidate our knowledge with real-world examples and sample code.
Practical use case
Imagine an e-commerce store where the product catalogue is viewed frequently by anonymous users. The heavy lifting of assembling product details can slow down the site if not optimised. By caching both raw query results and rendered output, you ensure the catalogue loads quickly under heavy traffic.
Sample implementation
/**
* Implements hook_views_default_views_alter().
*
* This configuration sets a tag-based caching strategy for the "product_catalog" view.
* It caches both the raw query results and the rendered HTML for 1 hour.
*/
function mymodule_views_default_views_alter(&$views) {
if (isset($views['product_catalog'])) {
$view = $views['product_catalog'];
$view->display['default']->display_options['cache'] = [
'type' => 'tag',
'options' => [
'results_lifespan' => '3600', // Cache the query results for 1 hour.
'output_lifespan' => '3600', // Cache the rendered output for 1 hour.
'output_tag' => 'product_list', // Invalidate cache when product data is updated.
],
];
}
}
Explanation: This sample demonstrates how a custom caching configuration improves performance for the product catalog by reducing repeated heavy queries and re-rendering, while ensuring that changes in product data clear the cache appropriately.
Practical use Case
A dynamic form like a checkout or profile update contains both static guidance (which benefits from caching) and dynamic, user-specific sections (which need to be rendered fresh). By applying caching only to the non-sensitive parts, you can improve speed without risking data leaks.
Sample implementation
/**
* Implements hook_form_alter().
*
* This function applies caching directives to the "my_dynamic_form",
* ensuring that while static elements are cached, dynamic content remains secure and fresh.
*/
function mymodule_form_alter(&$form, &$form_state, $form_id) {
if ($form_id === 'my_dynamic_form') {
$form['#cache'] = [
'contexts' => [
'user', // Unique cache per user.
'user.roles', // Tailored cache for different user roles.
'url.path', // Sensitive to the current page URL.
],
'tags' => ['user:' . \Drupal::currentUser()->id()], // Unique cache tag per user session.
];
}
}
Explanation: This code safely applies caching to non-dynamic elements of a form, ensuring that overall load times are improved while personal and sensitive parts are always freshly rendered.
We’ve delved into advanced caching techniques for Drupal views and forms, covering:
This article is part of our comprehensive 10-part series on Drupal caching:
Optimising Drupal views and forms with intelligent caching isn’t just a technical exercise, it’s a necessary step toward building fast, scalable, and resilient digital experiences. By applying a combination of time-based, tag-based, and contextual caching strategies, you can significantly reduce server load, improve response times, and deliver consistently reliable performance, even under high traffic.
The key is understanding the different layers of caching available, query result caching, rendered output caching, and selective form caching, and applying them thoughtfully based on the specific needs of each component. Balancing speed with security, especially when dealing with user-specific data, ensures that caching improvements don’t come at the cost of data integrity.
Mastery of these techniques transforms your Drupal site into a platform that not only handles today’s demands but remains agile enough for future growth. As caching strategies evolve, continuing to monitor, refine, and adapt your configurations will keep your site operating at its best.
In the next parts of this series, we’ll explore even more advanced caching methods, including entity caching, custom caching services, and performance monitoring best practices, helping you move from basic optimization to a truly high-performance Drupal architecture.
.avif)
Slow pages lose users. For Drupal sites, smart caching flips that script, turning heavy, database-driven pages into quick, memory-efficient responses. Whether it’s a blog, nonprofit platform, or enterprise CMS, caching is essential to performance.
Picture this: a Drupal 10 site serving dynamic content to thousands daily. Without caching, every page view hits the backend, increasing load times and straining servers. With internal and dynamic caching in place, pages for anonymous and authenticated users are served in milliseconds, not seconds.
In this article, we’ll break down:
These aren’t optional tweaks. They’re foundational for performance, reliability, and scale.
Internal Page Cache is Drupal’s way of speeding up pages for anonymous users. When someone visits a page, Drupal saves the fully rendered HTML. If another user visits the same page, Drupal serves that saved version instantly, skipping the usual backend processing.
This reduces load times and server work, especially on high-traffic sites. It’s simple, fast, and only applies to users who aren’t logged in.
Further Reading:
Check out the official Internal Page Cache documentation for more details.
The scenario
Imagine you manage a community news site that serves a large volume of anonymous visitors. Frequently, the site displays the same news articles repeatedly. Without caching, each page load requires Drupal to rebuild the entire page dynamically, which increases server load and slows down response times.
The goal
To optimise performance for anonymous users by reducing the processing time required for each page request. The aim is to serve fully rendered pages directly from the cache, thereby minimising database queries and PHP processing cycles.
Considerations
Sample implementation
Although Internal Page Cache is largely managed by Drupal core, you can fine-tune certain configurations in your settings.php:
// In settings.php, set up the Internal Page Cache for anonymous users.
$settings['cache']['bins']['page'] = 'cache.backend.internal_page_cache';
// Optionally, configure the maximum age for cached pages.
// This example sets the cache lifetime to 1 hour.
$config['system.performance']['cache']['page']['max_age'] = 3600;
This configuration ensures that the full HTML page is cached and reused for subsequent requests. For more detailed guidelines, refer to the Internal Page Cache documentation.
Dynamic Page Cache is designed for handling pages for authenticated users, whose pages often include dynamic or personalised content. It caches page fragments instead of the entire page, making it possible to mix cached and dynamic content seamlessly.
Further reading:
Visit the Dynamic Page Cache documentation for further details.
The scenario
Consider an e-commerce platform where registered users see personalised information, such as greeting messages, recent order details, or custom promotions. In such cases, parts of the page remain the same across many users, while other parts vary.
The goal
To improve performance for authenticated users by caching as much of the page as possible without compromising personal or dynamic content. By caching static fragments, the platform reduces the load on the server while still delivering personalised content where necessary.
Considerations
Sample implementation
Dynamic Page Cache is enabled in Drupal core, but you can complement it by configuring it in your settings. For example, you might not change its default behavior in settings.php but ensure that your custom modules use cache tags and contexts appropriately when rendering parts of the page.
tags and contexts appropriately when rendering parts of the page.
// In settings.php, configure Dynamic Page Cache for authenticated pages.
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.dynamic_page_cache';
In your custom module or theme, when rendering a personalized block, you might include cache metadata like this:
$build = [
'#markup' => t('Welcome back, @user!', ['@user' => \Drupal::currentUser()->getDisplayName()]),
'#cache' => [
'tags' => ['user:' . \Drupal::currentUser()->id()],
'contexts' => ['user.roles'],
'max-age' => 1800,
],
];
This example ensures that while the personalised greeting is rendered dynamically, it still benefits from partial caching of related fragments. For more details, visit the Dynamic Page Cache documentation.
Blocks are critical components of a Drupal site used in sidebars, headers, footers, and more. Proper caching of blocks can reduce server load considerably by ensuring that frequently used content is generated only once.
Drupal offers several caching configurations for blocks:
Further reading:
Learn more about block caching and cache tags on Drupal.org at Cache Tags.
The scenario
Imagine you have a custom block on a dashboard that displays quick links and summary statistics. Since different user roles (such as administrators versus regular users) need to see different information, caching must be configured to account for these variations.
The goal
To cache the block efficiently while allowing for role-based customisations. The objective is to minimise repeated block rendering without serving incorrect data to different user types.
Considerations
Sample implementation
Below are two examples. The first uses hook_block_alter() in a custom module to adjust the cache settings of an existing block, and the second shows how to define caching when creating a new block in your theme or module.
/**
* Implements hook_block_alter().
*
* Use case: Adjusting cache settings for a block that shows role-specific dashboard information.
*/
function mymodule_block_alter(&$blocks) {
if (isset($blocks['my_custom_block']) && $blocks['my_custom_block']['id'] === 'my_custom_block') {
$blocks['my_custom_block']['cache'] = [
'max-age' => 3600, // Cache for 1 hour.
'contexts' => ['user.roles'], // Cache varies by user role.
'tags' => ['custom_dashboard'],
];
}
}
Alternatively, when defining a custom block:
/**
* Implements hook_theme().
*/
function mymodule_theme() {
return [
'my_cacheable_block' => [
'render element' => 'content',
'cache' => [
'max-age' => 3600, // Cache for 1 hour.
'contexts' => ['user.roles'], // Cache per user role.
],
],
];
}
These examples ensure that the block content is cached efficiently while still allowing for customization based on user context.
Below are additional examples that show how to enable and adjust page caching directly from your settings.php:
// In settings.php:
// Enable Internal Page Cache.
$settings['cache']['bins']['page'] = 'cache.backend.internal_page_cache';
// Configure Dynamic Page Cache for authenticated pages.
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.dynamic_page_cache';
// Set a maximum age for cached pages, e.g., 1 hour.
$config['system.performance']['cache']['page']['max_age'] = 3600;
function mymodule_check_page_cache() {
$cache_backend = \Drupal::cache('page');
// Check for a specific cache entry.
$cache_entry = $cache_backend->get('my_page_cid');
if ($cache_entry) {
\Drupal::logger('mymodule')->info('Page cache hit for my_page_cid!');
}
else {
\Drupal::logger('mymodule')->warning('Page cache miss for my_page_cid.');
}
}This article is part of our comprehensive 10-part series on Drupal caching:
Page and block caching are essential tools in Drupal’s performance playbook. When implemented properly, they can reduce server strain, minimise database queries, and significantly improve how quickly pages are delivered to users.
Internal Page Cache serves pre-rendered HTML to anonymous visitors, allowing Drupal to bypass the full page build process. Dynamic Page Cache complements this by caching reusable parts of a page for logged-in users, even when the content is personalised. Combined with effective block caching, by configuring cache contexts, tags, and expiration times, Drupal can handle high volumes of traffic with far less effort on the backend.
Some sites have seen load times drop by as much as 80% for repeat page views just by using these built-in caching systems. That kind of performance boost improves everything from user experience to SEO.
To apply these strategies effectively:
Always tailor your caching setup to the structure and needs of your site, and reassess it as your content, audience, or features evolve.
.avif)
Optimising performance is critical for any high-traffic or feature-rich Drupal site. One of the most effective ways to improve responsiveness and reduce server load is by implementing a well-structured caching strategy.
While Drupal's built-in database caching may suffice for smaller or less complex sites, larger applications typically require more efficient solutions. Advanced cache backends like Redis, APCu, or Memcache can significantly enhance page load times by minimising repeated database queries and reducing latency across the application.
This blog walks through the most reliable caching options for modern Drupal environments, using current practices that apply across supported versions. You’ll find practical configuration examples and sample implementation code for each backend, providing a clear path to boost performance and deliver a smoother user experience.
Drupal stores cached data in “cache backends” – the physical mechanism (database, in-memory, or remote servers) where cached items reside. How cache items are stored, retrieved, and invalidated directly impacts performance, scalability, and resource consumption.
Key considerations:
For more details on the design and best practices of Drupal’s Cache API, refer to the official Drupal Cache API Documentation.
Drupal supports several caching solutions, each with its strengths:
Many administrators observe that transitioning from the default database cache to an advanced backend can result in significant performance improvements, especially as traffic increases.
The scenario
Imagine a popular travel website that initially relies on Drupal’s default database caching. As the site grows, increasing traffic results in a heavier load on the database, slowing page responses and escalating resource consumption.
The goal
To reduce the load on the database and boost overall responsiveness, the site migrates to an advanced caching backend. For example, configuring Redis enables the site to serve cached content rapidly from memory while capitalising on its clustering features for scalability.
Considerations
Below is a practical sample implementation that demonstrates how to fetch data from a third-party API, cache it using Drupal’s Cache API, and then render the data with appropriate cache metadata. This example illustrates the concept behind transitioning from heavy database queries to utilising a fast cache backend.
/**
* Fetches weather data from a third-party API and caches the result.
*
* This function demonstrates a practical use case where an expensive API call
* is cached to reduce load on both the external API and the database.
*
* @param int $location_id
* The unique identifier for a location.
*
* @return array
* A render array containing the weather data and cache metadata.
*/
function mymodule_get_weather_data($location_id) {
// Create a unique cache ID based on the location.
$cid = 'mymodule:weather:' . $location_id;
// Load the default 'data' cache bin.
$cache = \Drupal::cache('data');
// Attempt to retrieve cached data.
if ($cache_item = $cache->get($cid)) {
// Cache hit: return the cached data along with render metadata.
return [
'#theme' => 'weather_display',
'#data' => $cache_item->data,
'#cache' => [
'tags' => ['weather:' . $location_id],
'max-age' => 1800,
],
];
}
// Cache miss: perform the API call to fetch weather data.
$api_url = "https://api.example.com/weather?location={$location_id}";
$response = file_get_contents($api_url);
// Decode the JSON response.
$weather_data = json_decode($response, TRUE);
// Define cache tags and set an expiration (max-age of 30 minutes).
$tags = ['weather:' . $location_id];
$max_age = 1800;
$expire = time() + $max_age;
// Store the fetched data in cache.
$cache->set($cid, $weather_data, $expire, $tags);
// Build and return the render array with cache metadata.
return [
'#theme' => 'weather_display',
'#data' => $weather_data,
'#cache' => [
'tags' => $tags,
'max-age' => $max_age,
],
];
}For additional details on implementing caching strategies in Drupal, see the official Cache API documentation.
Below are the precise configurations to include in your settings.php for each cache backend. These examples have been verified for code accuracy and are accompanied by links to relevant Drupal.org documentation.
No additional module installation is needed for the default database caching. To explicitly set it, add the following to your settings.php:
// In settings.php, set the default cache backend to database.
$settings['cache']['default'] = 'cache.backend.database';
APCu Backend
Before configuring APCu, ensure the APCu PHP extension is installed and enabled on your server.
// In settings.php, configure APCu caching.
if (extension_loaded('apcu') && ini_get('apc.enabled')) {
// Use APCu as the default cache backend.
$settings['cache']['default'] = 'cache.backend.apcu';
// Optionally, assign APCu to specific cache bins.
$settings['cache']['bins']['render'] = 'cache.backend.apcu';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.apcu';
}The Drupal APCu Cache Module provides further instructions and documentation.
To use Redis, ensure the Redis PHP extension is installed and that the Redis module is added via Composer.
// In settings.php, configure Redis if the Redis extension and module are available.
if (extension_loaded('redis') && class_exists('Drupal\redis\ClientFactory')) {
// Define connection parameters.
$settings['redis.connection']['interface'] = 'PhpRedis';
$settings['redis.connection']['host'] = '127.0.0.1';
$settings['redis.connection']['port'] = 6379;
// Set Redis as the default cache backend.
$settings['cache']['default'] = 'cache.backend.redis';
// Optionally, exclude specific bins if required:
// $settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
// Load Redis service definitions.
$settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';
}For further configuration options, see the Redis Module for Drupal.
Memcache requires installing the Memcache server, enabling the PHP Memcache extension, and adding the corresponding Drupal module.
// In settings.php, configure Memcache if the extension and module are available.
if (extension_loaded('memcache') && class_exists('Drupal\memcache\MemcacheBackendFactory')) {
// Specify Memcache server settings.
$settings['memcache']['servers'] = ['127.0.0.1:11211' => 'default'];
$settings['memcache']['bins'] = ['default' => 'default'];
// Set Memcache as the default cache backend.
$settings['cache']['default'] = 'cache.backend.memcache';
// Load Memcache service definitions.
$settings['container_yamls'][] = 'modules/contrib/memcache/memcache.services.yml';
}More details can be found in the Memcache Module for Drupal.
When choosing a cache backend for your Drupal site, consider the following:
Once your caching backend is in place, continuous monitoring is essential:
// Example usage:
$cache = \Drupal::cache('render');
if (method_exists($cache, 'getStats')) {
$stats = $cache->getStats();
// Log or inspect $stats as needed.
}For additional guidance, refer to the Drupal Cache API Documentation.
This article is part of our comprehensive series on Drupal caching:
Implementing an effective caching strategy in Drupal is not just a performance enhancement, it's essential for maintaining speed and reliability at scale. As your site grows in complexity or traffic volume, relying solely on default database caching can introduce bottlenecks. Solutions like APCu, Redis, and Memcache offer flexible, powerful alternatives tailored to different infrastructure needs.
By choosing the right backend and applying the configuration steps outlined in this guide, you can significantly reduce server load, improve response times, and deliver a more seamless experience to your users.
As with any performance optimization, continuous testing and monitoring are key. Stay aligned with Drupal’s official documentation to adapt your caching setup as technologies and best practices evolve.

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.
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.
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:
Understanding these methods lays the groundwork for implementing effective caching strategies in your modules.
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.
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.
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;
}
Cache metadata is crucial in Drupal’s caching strategy. It controls how and when cached content should be invalidated.
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 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:
For a more in-depth discussion on cache metadata, please check the official Drupal caching API documentation.
Drupal organises cached data into separate “bins.” Each bin is a distinct storage container, designed to optimise different types of cache entries:

Automatically generating meaningful alt text for images in CKEditor—without writing a single line of code—is now within reach. AI-powered automation improves accessibility and streamlines content creation, allowing teams to focus on what matters most.
Alt text plays a key role in creating inclusive digital experiences, especially for users who rely on screen readers. While it’s a small part of the content workflow, it has a big impact—and AI can make it even easier to get right.
In this blog, we’ll look at how to build an AI Automator in CKEditor that generates alt text for images automatically. For example, when a content creator inserts an image of a chart comparing annual CO₂ emissions by country, the AI Automator can instantly generate alt text like “Bar chart comparing annual CO₂ emissions in the US, China, India, and EU from 2015 to 2024.”
The result: more accessible content, smoother publishing, and one less manual step.
The AI Automator module in Drupal allows for seamless AI-powered workflows inside CKEditor. Automator Chains type helps you chain multiple AI processes together (e.g., image analysis → text generation). AI can:
Alt text is crucial for accessibility, helping visually impaired users understand images. Instead of manually adding alt text for each image.
To create an AI Automator Chain Type for generating alt text from images, you first need to navigate to the Automator Chain Types configuration page. After adding a new chain type, you will define input and output fields to process the image and store the generated alt text.

You now need to edit the Image Alt Text field, configure the AI Automator settings, define the input mode and prompt, and set the AI provider.



To enable AI Automator for Output Image with Alt Text, you need to similar settings with appropriate prompt.


Enable AI Automator in CKEditor, by configuring the text format settings, here you will add the AI CKEditor plugin for the text format and enable AI Automators, and setting up the image alt text generation settings for ckeditor.


To test the AI Automator Chain Type, you should now create an article and insert an image in CKEditor.


This feature can be extended for media reference entities as well once the ticket is closed.
This AI-powered CKEditor assistant can be useful for:
Accessibility Tools: Improve image descriptions for visually impaired users.
SEO Optimization: Enhance image searchability with meaningful alt text.
Content Automation: Streamline the process of adding alt text to images.
AI assistants in CKEditor are already reshaping content creation. With the AI Automator module for Drupal, you can generate meaningful alt text for images—without writing a single line of code. From improving accessibility to enhancing SEO and removing repetitive steps, AI is making content workflows faster and more intentional.
A 2024 study found that 88% of marketers use AI daily, and 68% of service professionals rely on it for content creation. Even small businesses are part of the shift—98% use AI enabled services and 40% are experimenting with generative models.
Drupal is keeping pace. With support for OpenAI, Azure, Cohere, and others, its growing AI ecosystem brings model-based automation directly into the editor. The AI Automator handles everything from alt-text suggestions to smart content tweaks—already used by nonprofits, publishers, and public agencies across the US, India, and Europe.
This is the start of model-driven publishing: CMS experiences that work with you. Try the AI Automator, build your assistant in CKEditor, and be part of what’s next.
.avif)
Performance is a key part of delivering smooth, responsive digital experiences—and Drupal comes with powerful caching tools to help make that happen. When configured well, these built-in capabilities can dramatically reduce load times and improve the experience for every visitor.
For instance, one major university used strategic caching to optimize its Drupal-powered course catalog. During peak registration periods, pages that previously loaded in 6–8 seconds began responding in under 500ms—a 92% improvement. No hardware upgrades. Just a smarter configuration.
In this series, we’ll walk through everything you need to know about caching in Drupal 9, 10, and 11. From core settings to advanced techniques, we’ll look at how to apply caching effectively—backed by real results from production sites.
Caching is the single most effective performance optimization for Drupal sites. Here’s why it matters:
A website’s perceived performance directly affects user satisfaction and conversion rates. Studies show that conversion rates drop by an average of 4.42% for each additional second of load time, and 40% of visitors will abandon a site that takes more than 3 seconds to load.
Drupal’s caching capabilities have matured significantly across recent versions:
Drupal 10 enhanced these capabilities with:
Drupal 11 takes caching to the next level with:
While the core concepts remain consistent across versions, understanding these evolutionary improvements will help you leverage the full potential of your specific Drupal version.
Drupal employs multiple caching mechanisms that work together for maximum performance:
Each type of caching addresses specific performance challenges, and the full power comes from using them in combination.
Different caching strategies suit different scenarios:

Some content needs to go live at exactly the right time and without scheduling, that timing depends on someone being online, whether it’s late at night, on a weekend, or across time zones. Even during working hours, manually publishing content adds unnecessary coordination and risk.
Automation solves this. When content transitions are scheduled in advance, publishing becomes reliable, predictable, and hands-free. Teams can shift focus to strategy and execution—without the overhead of timing logistics.
While Drupal offers strong content moderation and workflows, the Scheduled Transitions module adds the missing piece—time-based scheduling for content state changes, adding precise, revision-level scheduling for any moderated content—so content moves exactly when it should, without manual effort.
On large, content-heavy websites, teams often need to publish, update, or unpublish content automatically at specific times. This includes campaign pages, release announcements, press embargoes, seasonal banners, and other time-sensitive updates.
Drupal’s core features like Content Moderation and Workflows support revisioning and moderation states (Draft, Needs Review, Published), but they don’t support scheduled transitions out of the box. This leads to a few technical and operational challenges:
Without a reliable scheduling mechanism, content can go live too early, too late, or not at all—creating gaps in campaigns, missed deadlines, or outdated messaging.
The Scheduled Transitions module allows automated, revision-specific scheduling for any moderated entity (like nodes, custom blocks, etc.). It supports granular permissions, multilingual scheduling, and queue-based processing that doesn’t overload the default cron—making it a solid fit for time-sensitive content workflows in Drupal.
It works across all content entities that use workflows and moderation and supports revision-specific scheduling. Editors can pick the exact revision they want to publish and schedule that transition directly from the UI.
The module provides:
Instead of relying on Drupal’s site-wide cron (which isn’t designed to run every minute), it uses custom queue jobs:
This lets scheduling run every minute without affecting site performance. If needed, the module can also run via default site cron at a longer interval.
By using Scheduled Transitions, editorial teams can automate time-sensitive publishing workflows, avoid manual errors, and remove the need to be online during off-hours. The result: reliable, accurate, and maintainable content scheduling across complex Drupal sites.




Let’s understand the use of this module with the example. The following example uses a flow of draft to publish, but any transition is possible. The general flow for users is:


The Scheduled Transitions module fills a key gap in Drupal’s content moderation system: the ability to automate publishing based on scheduled dates and times for specific revisions. It ensures content goes live exactly when planned, without any manual effort.
Over 1,000 Drupal sites use the module, including government platforms, universities, newsrooms, and nonprofits. It supports multilingual content, strict timelines, and globally distributed workflows.
But scheduling is just the start. Drupal’s next phase is being shaped by AI-assisted publishing.
Features like auto-tagging, translation support, and semantic search are already in use. Organizations like Acquia are adding AI/ML to Drupal DXP stacks, while community-led projects explore LLM integrations.
Governments and global organisations are leading this shift. Drupal powers platforms for the European Commission, UNESCO, the City of Boston, and UNICEF. Major brands like NASA, Pfizer, and The Economist also use Drupal at scale—integrating AI to optimize publishing.
The trend is clear: AI content automation is projected to grow 18.7% annually through 2030. Scheduling will move beyond fixed dates to AI-informed windows based on engagement, traffic, and campaign timing.
Modules like Scheduled Transitions will evolve with this shift—helping Drupal build smarter, data-driven workflows for modern content teams.