Back

Loading…

Documentation

🧠 Cacheability en Drupal: cómo funcionan tags, contexts y max-age

En el artículo anterior vimos que Drupal no tiene una única caché y que, antes de pensar en Redis, Varnish o un CDN, debemos entender si Drupal está produciendo contenido correctamente cacheable.

Ahora toca entrar en el corazón de ese sistema.

Drupal describe la cacheabilidad mediante tres conceptos fundamentales:

Cache Tags
Cache Contexts
Cache Max-Age

Podemos pensar en ellos como tres preguntas diferentes:

Cache Tags
→ ¿De qué datos depende este resultado?

Cache Contexts
→ ¿En qué contexto puede cambiar?

Cache Max-Age
→ ¿Durante cuánto tiempo puede reutilizarse?

Estas tres piezas permiten que Drupal haga algo mucho más inteligente que simplemente guardar HTML durante algunos minutos.

Le permiten saber cuándo reutilizar una respuesta, cuándo crear una variante y cuándo invalidarla.


🏷️ Cache Tags: ¿de qué datos depende este contenido?

Imaginemos que renderizamos un nodo:

Node 42

Drupal puede asociar a ese resultado una cache tag como:

node:42

Conceptualmente:

Render result
└── depends on node:42

Mientras el nodo 42 no cambie, Drupal puede reutilizar el resultado.

Pero cuando alguien edita ese nodo:

Node 42 updated
      ↓
invalidate node:42
      ↓
cached results depending on node:42
become invalid

Ese es el objetivo principal de los cache tags.

Permiten invalidar la caché cuando cambian los datos de los que depende.

Drupal documenta los cache tags precisamente como el mecanismo para rastrear dependencias de datos e invalidarlas cuando esos datos cambian. (drupal.org)


🧩 Un componente puede depender de muchas cosas

Pensemos en una página de detalle de producto.

Podría depender de:

product:24
taxonomy_term:12
media:55
config:system.site

Si cualquiera de esos elementos cambia, alguna parte de la página podría necesitar regenerarse.

Por eso Drupal puede combinar múltiples tags:

[
  "commerce_product:24",
  "taxonomy_term:12",
  "media:55",
  "config:system.site"
]

No tenemos que decidir manualmente:

“Esta página debe expirar dentro de cinco minutos.”

Drupal puede saber de forma mucho más precisa qué cambio hace obsoleto el contenido.


🗂️ Cache tags de listas

Hay otro concepto muy útil.

Supongamos que tenemos una View mostrando artículos:

Latest news
├── Article A
├── Article B
└── Article C

No solamente queremos invalidar el resultado cuando cambia uno de esos artículos.

También queremos invalidarlo si aparece:

Article D

porque ahora la lista debería cambiar.

Drupal utiliza para esto tags de listas, conceptualmente similares a:

node_list

o tags más específicos según el tipo de entidad.

Esto permite representar la diferencia entre:

"este nodo cambió"

y:

"la colección de nodos cambió"

Es una distinción muy poderosa en páginas construidas con Views.


🌍 Cache Contexts: ¿para quién o en qué situación cambia?

Ahora imaginemos otro escenario.

Tenemos el mismo contenido, pero el resultado cambia dependiendo del idioma:

Visitor A
Language: German
→ Produkte

Visitor B
Language: English
→ Products

No queremos invalidar la caché constantemente.

Tampoco queremos declarar:

max-age: 0

Solo necesitamos decirle a Drupal:

Este resultado tiene diferentes variantes según el idioma.

Ahí entran los cache contexts.

Drupal los describe como el equivalente conceptual al header HTTP:

Vary

(drupal.org)


🧠 Un cache context crea variantes

Si un elemento tiene:

languages:language_interface

podemos imaginar:

Cached component
├── German variant
├── English variant
└── French variant

Drupal no está diciendo:

“Esto no se puede cachear.”

Está diciendo:

“Esto se puede cachear, pero necesito distinguir varias versiones.”

Esa diferencia es fundamental.


👤 Contextos de usuario

Otro ejemplo típico:

user.permissions

Un bloque puede mostrarse de forma diferente dependiendo de los permisos del usuario.

Drupal podría mantener distintas variantes según esos permisos.

Lo importante es no saltar directamente a:

max-age: 0

simplemente porque algo es personalizado.

Muchas personalizaciones pueden expresarse correctamente mediante cache contexts.


🔎 Algunos contextos habituales

Drupal incluye contextos relacionados con cosas como:

route
url
url.query_args
languages
user
user.permissions
user.roles
theme
timezone

Cada uno representa una dimensión potencial de variación.

Por ejemplo:

url.query_args:page

puede significar:

/news?page=0
/news?page=1
/news?page=2

Son respuestas diferentes, pero cada una puede seguir siendo cacheable.


⚠️ Más contexts no siempre significa mejor

Aquí aparece otro aspecto importante.

Supongamos que un bloque declara:

user

Eso potencialmente genera una variante por usuario.

Con:

10 users

puede no importar demasiado.

Pero con:

100,000 users

puede generar una enorme cantidad de variantes.

Quizá el componente realmente solo dependía de:

user.roles

o:

user.permissions

En ese caso utilizar user sería demasiado específico.

Podemos pensar en esto como cardinalidad de caché:

language
→ pocas variantes

role
→ algunas variantes

permissions
→ más variantes

user
→ potencialmente miles o millones

Por eso elegir correctamente los cache contexts también es una decisión de rendimiento.


⏱️ Cache Max-Age: ¿cuánto tiempo puede reutilizarse?

El tercer elemento es:

max-age

Drupal lo utiliza para describir durante cuánto tiempo un resultado puede considerarse válido.

Por ejemplo:

'#cache' => [
  'max-age' => 3600,
]

significa conceptualmente:

cacheable for one hour

Drupal utiliza segundos para representar este valor. (drupal.org)


♾️ Permanent

Un elemento puede ser válido hasta que alguna de sus dependencias sea invalidada.

En Drupal esto se representa normalmente mediante:

Cache::PERMANENT

No significa:

“Guardar para siempre aunque cambie todo.”

Significa más bien:

“No existe una expiración basada en tiempo; utiliza invalidación.”

Por ejemplo:

Node render
max-age = permanent
tag = node:42

puede permanecer cacheado indefinidamente…

hasta que:

node:42

sea invalidado.

Esta combinación es mucho más precisa que una expiración arbitraria de cinco minutos.


🔴 max-age: 0

El valor que más atención merece es:

max-age: 0

Significa que el resultado no es cacheable.

Esto puede ser legítimo.

Por ejemplo, quizá un componente realmente necesita mostrar información completamente dinámica en cada request.

Pero debemos utilizarlo con cuidado.

Porque Drupal combina la metadata de cacheabilidad.

Y ahí empieza uno de los problemas más comunes.


🫧 La metadata de caché “sube”

Imaginemos esta estructura:

Page
└── Main content
    └── Product
        └── Price block

Cada nivel tiene metadata.

Por ejemplo:

Page
max-age: permanent

Product
max-age: permanent

Price block
max-age: 0

Cuando Drupal combina esta información, el elemento superior debe respetar la restricción más fuerte.

Podemos pensar:

permanent
    +
permanent
    +
0
    =
0

Es decir:

un pequeño componente no cacheable puede hacer que un resultado mucho mayor deje de ser cacheable.

Este comportamiento se conoce habitualmente como cacheability bubbling.

La documentación del Render API explica precisamente que los cache tags, contexts y max-age se propagan desde los elementos hijos hacia sus padres. (drupal.org)


🧩 Un ejemplo sencillo con Render API

Un render array puede declarar:

$build = [
  '#markup' => 'Hello',
  '#cache' => [
    'tags' => [
      'node:42',
    ],
    'contexts' => [
      'languages:language_interface',
    ],
    'max-age' => 3600,
  ],
];

Aquí estamos diciendo:

Data dependency
→ node:42

Variation
→ interface language

Time
→ 3600 seconds

Es una descripción bastante completa del comportamiento de caché del componente.


🔗 Cache metadata debe acompañar a los datos

Este principio es extremadamente importante cuando desarrollamos código custom.

Imaginemos que cargamos un nodo:

$node = Node::load(42);

y generamos manualmente:

$build['title'] = [
  '#markup' => $node->label(),
];

Nuestro HTML depende del nodo 42.

Por tanto, esa dependencia debería aparecer también en la metadata de caché.

Si no lo hacemos, podemos crear un problema muy peligroso:

Node changes
↓
Drupal doesn't know cached output depends on it
↓
stale content

Por eso Drupal trabaja mucho con interfaces como:

CacheableDependencyInterface

y helpers que permiten propagar automáticamente la metadata de objetos cacheables.


🛠️ CacheableMetadata

Una herramienta muy útil en código custom es:

use Drupal\Core\Cache\CacheableMetadata;

Por ejemplo:

$cacheability = CacheableMetadata::createFromObject($node);
$cacheability->applyTo($build);

Ahora nuestro render array hereda correctamente la cacheability metadata de la entidad.

La idea es sencilla:

Si nuestro output depende de algo, su metadata de caché también debe depender de ese algo.

Drupal documenta CacheableMetadata precisamente como una forma de combinar y aplicar cacheability metadata. (api.drupal.org)


🧠 Los tres conceptos trabajan juntos

Supongamos un bloque:

"Welcome, Daniel"

Podría tener:

tags
→ user:42

contexts
→ user

max-age
→ permanent

Esto significa:

El contenido depende de user 42

Existe una variante según el usuario

Puede permanecer cacheado
hasta que cambien sus dependencias

Eso es muy diferente de:

max-age: 0

Ambos pueden producir contenido personalizado.

Pero el primero puede aprovechar la caché.


🎯 Invalidar no es lo mismo que expirar

Esta diferencia merece una sección propia.

Expiración

max-age: 600

significa:

Después de 600 segundos deja de considerarse válido.

Invalidación

node:42

significa:

Sigue siendo válido hasta que cambie el nodo 42.

Drupal favorece mucho este segundo modelo.

Por ejemplo:

Article doesn't change for 3 days

Con:

max-age: 300

podríamos reconstruirlo cientos de veces innecesariamente.

Con:

max-age: permanent
tags: node:42

podemos reutilizarlo durante los tres días completos y regenerarlo exactamente cuando cambie.

Eso es mucho más eficiente.


🌐 ¿Cómo se relaciona esto con HTTP?

Aquí debemos mantener separados dos niveles.

Drupal puede tener internamente:

tags
contexts
max-age

para gestionar su render cache.

Después la respuesta final genera headers HTTP como:

Cache-Control

Un browser o CDN no entiende:

node:42

como cache tag de Drupal.

Pero Drupal sí utiliza toda esa metadata para decidir qué tipo de respuesta puede construir finalmente.

Por eso:

Drupal cacheability
        ↓
response cacheability
        ↓
HTTP headers

están relacionados, pero no son exactamente lo mismo.


🪆 Cacheability en componentes anidados

Podemos visualizar una página Drupal así:

Page
│
├── Header
│   ├── Logo
│   └── Main navigation
│
├── Content
│   ├── Node
│   ├── Media
│   └── Related View
│
└── Footer

Cada componente puede aportar:

tags
contexts
max-age

Drupal combina todo esto.

Por ejemplo:

Main navigation
tags:
  config:system.menu.main

Node
tags:
  node:42

Media
tags:
  media:8

Related View
tags:
  node_list

La página puede terminar acumulando:

config:system.menu.main
node:42
media:8
node_list

Así, si cambia cualquiera de esas dependencias, Drupal sabe qué debe invalidar.


⚠️ El error clásico: desactivar caché para solucionar un problema

Durante desarrollo aparece a veces una solución rápida:

'#cache' => [
  'max-age' => 0,
]

y aparentemente:

“Ahora sí se actualiza correctamente.”

Pero quizá el problema real era que faltaba:

cache tag

o:

cache context

Por ejemplo, si algo cambia según idioma y no declaramos:

languages:language_interface

podemos ver contenido incorrecto.

Poner:

max-age: 0

puede ocultar el bug.

Pero a cambio hemos eliminado la caché.

Una regla útil durante desarrollo es:

Cuando sentimos que necesitamos max-age: 0, primero preguntémonos si lo que realmente falta es una tag o un context.


🔍 Cache contexts también pueden causar problemas

El problema contrario también existe.

Supongamos que añadimos:

user

a toda una página únicamente porque un pequeño icono cambia para cada usuario.

Ahora podemos generar una variante completa de página para cada usuario.

Tal vez habría sido mejor aislar ese pequeño componente dinámico mediante:

lazy builder

o alguna otra estrategia dinámica.

Aquí vemos nuevamente que optimizar Drupal no consiste simplemente en:

"añadir caché"

sino en:

describir correctamente la cacheability

💤 Lazy builders y contenido dinámico

Drupal puede retrasar el renderizado de determinadas partes mediante:

#lazy_builder

y placeholders.

Esto permite situaciones conceptuales como:

Page
├── 95% cached
└── Cart block dynamically generated

en lugar de:

Entire page
max-age: 0

Esta arquitectura es fundamental para funcionalidades como BigPipe y para páginas con pequeñas zonas personalizadas.

Entraremos en esto con más detalle cuando hablemos de usuarios autenticados y Drupal Commerce.


🛒 Un adelanto de Drupal Commerce

Imaginemos una página de producto:

Product
├── Name
├── Description
├── Gallery
├── Price
└── Add to cart

Quizá:

Name
Description
Gallery

son altamente cacheables.

Pero:

Price
Cart state
Availability

pueden depender de contexto adicional.

La solución ideal normalmente no es:

entire page = uncacheable

sino describir correctamente esas dependencias y aislar las partes realmente dinámicas.

Ahí es donde Dynamic Page Cache empieza a mostrar todo su valor.


🧪 ¿Cómo podemos inspeccionar esta metadata?

Durante desarrollo Drupal puede exponernos información adicional.

En services.yml podemos habilitar determinados headers de debugging.

Entonces podemos observar cosas como:

X-Drupal-Cache-Tags
X-Drupal-Cache-Contexts

Esto nos permite ver directamente que una respuesta depende, por ejemplo, de:

languages:language_interface
route
user.permissions

o de tags como:

node:42
config:system.site

Drupal documenta estos mecanismos precisamente para depurar cacheability. (drupal.org)


🧠 Una forma práctica de diagnosticar

Cuando encontramos una página que parece no cachearse correctamente, podemos hacernos tres preguntas.

1. 🏷️ ¿Qué datos utiliza?

Por cada dato importante:

node?
term?
user?
config?
commerce product?

preguntamos:

¿Existe una cache tag correspondiente?


2. 🌍 ¿Cuándo cambia el resultado?

Por ejemplo:

language?
route?
query parameter?
role?
permissions?
user?
session?

preguntamos:

¿Tenemos el cache context apropiado?


3. ⏱️ ¿Realmente depende del tiempo?

Solo entonces preguntamos:

¿Necesita un max-age específico?

Muchas veces la respuesta será:

No.
It can remain cached until invalidated.

📐 Podemos pensar en cacheability como una función

Una manera bastante útil de visualizar el sistema es:

Cached result =
Data
×
Context
×
Time

Donde:

Data
→ cache tags

Context
→ cache contexts

Time
→ max-age

Si describimos correctamente esas tres dimensiones, Drupal puede reutilizar el resultado de manera segura.


🔁 ¿Qué pasa cuando Drupal combina dos componentes?

Supongamos:

Component A

tags:
  node:42

contexts:
  language

max-age:
  permanent

y:

Component B

tags:
  user:7

contexts:
  user.permissions

max-age:
  300

El contenedor debe representar las dependencias de ambos.

Conceptualmente:

tags:
  node:42
  user:7

contexts:
  language
  user.permissions

max-age:
  300

Para max-age, la restricción más corta gana.

Y si uno tiene:

max-age: 0

entonces:

combined max-age = 0

Ese detalle explica por qué un pequeño componente puede afectar tanto al comportamiento de una página.


🧩 Bubbleable Metadata

En Render API podemos encontrarnos con el término:

Bubbleable Metadata

incluye información como:

cacheability metadata
attachments

La palabra “bubbleable” describe precisamente la idea:

child
  ↑
metadata bubbles
  ↑
parent
  ↑
page

Entender este mecanismo ayuda muchísimo cuando desarrollamos:

  • custom blocks;
  • preprocess functions;
  • controllers;
  • custom render arrays;
  • Views plugins;
  • Commerce components.

✅ Buenas prácticas para código custom

Podemos resumir varias reglas.

🏷️ Si utilizamos una entidad

Propagar su cacheability metadata.

🌍 Si el output cambia según algo del request

Añadir el cache context correspondiente.

⏱️ No utilizar max-age: 0 por defecto

Antes debemos demostrar que el componente realmente no puede cachearse.

🧩 Mantener la metadata junto con el render array

No separar la lógica del contenido de su comportamiento de caché.

💤 Aislar pequeñas partes dinámicas

En lugar de hacer no cacheable todo el contenedor.


🚩 Algunas señales de alerta

Cuando revisemos código Drupal, estas líneas deberían hacernos mirar dos veces:

'#cache' => [
  'max-age' => 0,
]

También:

Cache::invalidateTags(...)

ejecutado indiscriminadamente.

O:

cache context: user

en componentes muy grandes.

No significa necesariamente que estén mal.

Significa:

Aquí hay una decisión importante de cacheability que debemos entender.


🧪 Un pequeño ejemplo completo

Imaginemos un bloque custom que muestra el título del nodo actual.

Podríamos construirlo así conceptualmente:

$build = [
  '#markup' => $node->label(),
  '#cache' => [
    'tags' => $node->getCacheTags(),
    'contexts' => [
      'route',
    ],
    'max-age' => $node->getCacheMaxAge(),
  ],
];

Ahora tenemos:

route
→ el nodo actual puede cambiar según la ruta

node tags
→ si cambia el nodo, invalidar

max-age
→ heredar comportamiento temporal

Pero Drupal nos ofrece mecanismos para propagar esa metadata sin reconstruir todo manualmente.

La idea fundamental permanece:

El HTML y su cacheability metadata son dos partes de la misma respuesta.


🏗️ Volvamos a nuestra arquitectura

En el artículo anterior vimos algo parecido a:

Browser
   ↓
CDN
   ↓
Reverse Proxy
   ↓
Drupal Page Cache
   ↓
Dynamic/Render Cache
   ↓
Cache Backend

Ahora podemos añadir otra capa conceptual:

Drupal Render System
        ↓
Cache Tags
Cache Contexts
Cache Max-Age
        ↓
Dynamic / Render Cache
        ↓
Page response

Estos tres valores son parte de la información que permite a las demás capas tomar decisiones correctas.


✅ La idea principal

Si queremos recordar una sola cosa:

Cacheability no significa decidir si algo está cacheado o no. Significa describir correctamente cuándo un resultado puede reutilizarse.

Drupal necesita conocer tres cosas:

🏷️ Tags: de qué datos depende. 🌍 Contexts: en qué situaciones cambia. ⏱️ Max-age: durante cuánto tiempo es válido.

Cuando esa metadata es correcta, Drupal puede hacer cosas muy potentes:

  • reutilizar contenido;
  • invalidarlo exactamente cuando cambia;
  • mantener diferentes variantes;
  • combinar componentes;
  • renderizar dinámicamente solo lo necesario.

Y cuando esa metadata es incorrecta, aparecen dos clases de problemas:

Too much caching
→ stale / incorrect content

Too little caching
→ unnecessary work / poor performance

El objetivo está justo en medio:

Cachear todo lo que sea seguro cachear y regenerar solamente lo que realmente cambió.


🔜 En el siguiente artículo

Ya sabemos cómo Drupal describe la cacheabilidad.

Ahora podemos utilizar ese conocimiento para investigar problemas reales.

¿Qué significa exactamente:

X-Drupal-Dynamic-Cache: HIT

frente a:

MISS

o:

UNCACHEABLE

¿Cómo podemos descubrir qué bloque está introduciendo un max-age: 0?

¿Qué podemos revisar con curl, DevTools, Webprofiler y los headers de debug?

👉 En el siguiente artículo pasaremos de la teoría al diagnóstico con 🔍 “Cómo investigar una página que Drupal no quiere cachear”.

Contents