Perché questa pagina Drupal non va in cache?

Approfondisci il progetto Native Observability
Drupal lo scrive nelle intestazioni della risposta, e la risposta sparisce appena arriva. Native Observability conserva per ogni richiesta l'esito della page cache e della dynamic page cache, i tag, i contesti e il max-age, e le invalidazioni di tag che la richiesta ha causato. Registra anche le richieste servite dalla page cache, che Drupal non gestisce.

Come si scopre perché una pagina Drupal non è andata in cache?

Si legge il motivo che Drupal stesso scrive sulla risposta, a patto di averlo conservato. La page cache e la dynamic page cache scrivono il loro esito nelle intestazioni X-Drupal-Cache e X-Drupal-Dynamic-Cache: HIT, MISS oppure UNCACHEABLE con la regola che l'ha deciso fra parentesi. Quelle intestazioni arrivano al browser e lì finiscono. Il giorno dopo, quando qualcuno chiede perché una pagina era lenta, la risposta è già persa.

Native Observability è un modulo Drupal che registra dall'interno che cosa succede a ogni richiesta. Il sottomodulo Cache Observer, native_observability_cache_observer, salva nel database l'esito di ogni strato di cache, i metadati di cacheabilità della risposta e le invalidazioni di tag, legati alla rotta e alla traccia della stessa richiesta. Le query lente della stessa richiesta sono nell'articolo sulle query che rallentano una rotta, le chiamate verso servizi esterni in quello sul servizio esterno che rallenta una rotta.

Le righe riportate qui vengono da un ambiente di misura che ho costruito io, con Drupal 11.4.5 in DDEV. Come rifarlo sta in fondo, nella sezione «Come rifare la misura».

Che cosa registra Cache Observer per ogni richiesta

Tre tipi di evento, ognuno nella tabella native_observability_cache_event con l'identificativo della richiesta che l'ha prodotto.

Tipo di evento Strato Che cosa contiene Da dove lo prende
response_cache page_cache HIT, MISS o UNCACHEABLE (motivo) intestazione X-Drupal-Cache, letta da un middleware
response_cache dynamic_page_cache stessi valori intestazione X-Drupal-Dynamic-Cache, letta in kernel.response
cacheability response_cacheability numero ed elenco di tag e contesti, max-age metadati della risposta, getCacheableMetadata()
tag_invalidation cache_tags i tag invalidati da una chiamata un invalidatore aggiunto a quelli di core

I metadati di cacheabilità vengono letti dalla risposta, dagli stessi dati da cui core ricava i debug header. Il parametro http.response.debug_cacheability_headers resta quindi a false, come chiede il commento di default.services.yml: «Enabling cacheability debugging is not recommended in production environments». Tutte le prove di questa pagina sono state fatte con quel parametro spento.

Una pagina che non va in cache: le tre righe che lo spiegano

Una GET /node/XXX/edit fatta da un utente autenticato lascia tre righe con lo stesso identificativo di richiesta, XXXXXXXXXXXXXXXXXXXXXXMJ7M, e la stessa rotta, entity.node.edit_form:

Strato Esito registrato
page_cache UNCACHEABLE (REQUEST POLICY)
dynamic_page_cache UNCACHEABLE (POOR CACHEABILITY)
response_cacheability 22 tag, 11 contesti, max-age 0

Ogni etichetta corrisponde a una regola precisa del core.

  • REQUEST POLICY sulla page cache: la richiesta ha una sessione. La page cache serve solo gli

anonimi, e la decisione avviene prima ancora di cercare la pagina.

  • POOR CACHEABILITY sulla dynamic page cache: la risposta rientra in una delle

auto_placeholder_conditions di renderer.config. Il metodo shouldCacheResponse() la scarta quando il max-age è 0, quando c'è un contesto ad alta cardinalità come user o session, oppure un tag che si invalida troppo spesso.

  • La riga di cacheabilità dice quale delle tre condizioni è scattata: max-age 0. Nel campo

payload ci sono anche i nomi, fra cui i contesti user e session.exists.

Altri due esiti visti nella stessa misura completano il quadro. user.reset.login, il link di accesso monouso, dà UNCACHEABLE (RESPONSE POLICY): la pagina è stata prodotta, e una regola sulla risposta ne ha impedito il salvataggio. Una risposta che non porta metadati di cacheabilità dà UNCACHEABLE (NO CACHEABILITY) su entrambi gli strati.

Il HIT della page cache, che Drupal non vede

Una pagina servita dalla page cache viene registrata anche se Drupal non l'ha mai gestita. La page cache è il middleware http_middleware.page_cache, a priorità 200, e su un HIT restituisce la risposta salvata prima che il kernel HTTP parta: niente routing, niente kernel.response, niente kernel.terminate. Per il resto di Drupal quella richiesta non è esistita.

Cache Observer si mette sopra, con un middleware a priorità 210, CachePageObserverMiddleware, legge X-Drupal-Cache sulla risposta che torna indietro e scrive la riga. Visto che il kernel non ha assegnato un identificativo, lo conia il middleware e lo mette sulla risposta in X-Native-Observability-Request-Id.

Due richieste anonime alla stessa pagina, una dopo l'altra:

a1.txt:x-drupal-cache: MISS
a1.txt:x-native-observability-request-id: XXXXXXXXXXXXXXXXXXXXXXP9EJ
a2.txt:x-drupal-cache: HIT
a2.txt:x-native-observability-request-id: XXXXXXXXXXXXXXXXXXXXXXP40A

E nel database, con il numero di righe di traccia che ogni identificativo ritrova:

id    request_id                  status  path                                 route_name             trace_rows
XXXX  XXXXXXXXXXXXXXXXXXXXXXP9EJ  MISS    /review/what-changed-in-drupal-11-4  entity.node.canonical  1
XXXX  XXXXXXXXXXXXXXXXXXXXXXP40A  HIT     /review/what-changed-in-drupal-11-4                         0

Il HIT ha zero righe di traccia e la rotta vuota, ed è la risposta esatta: Drupal non ha instradato quella richiesta e non l'ha tracciata. Per un HIT il dato che resta è il percorso. Contare i HIT di una pagina si fa quindi per path, perché route_name c'è solo sulle richieste arrivate al kernel.

L'identificativo di un'altra richiesta, dentro una risposta in cache

La page cache salva la risposta intera, intestazioni comprese. Un sito che aggiunge a ogni risposta un identificativo di correlazione, per ritrovare la richiesta nei log, se lo ritrova salvato insieme alla pagina: al secondo visitatore arriva l'identificativo del primo. Cercandolo nei log si trova la richiesta di un'altra persona, magari di ore prima.

Cache Observer lo toglie. Il middleware riconosce una risposta servita dalla cache dall'assenza dell'attributo che il kernel mette su ogni richiesta gestita, rimuove l'intestazione vecchia e scrive quella nuova. Le due richieste qui sopra lo mostrano: identificativi diversi, uno per richiesta.

Lo stesso effetto tocca un'intestazione di core. Sul HIT la risposta riporta ancora x-drupal-dynamic-cache: MISS, che è l'esito della dynamic page cache per la richiesta che ha riempito la page cache. Su un HIT la dynamic page cache non ha lavorato affatto. Chi legge le intestazioni a mano per capire la cache di una pagina anonima deve saperlo: su un HIT della page cache, X-Drupal-Dynamic-Cache descrive un'altra richiesta.

Quale invalidazione ha svuotato la pagina

Cache Observer affianca agli invalidatori di core un suo invalidatore, ObservedCacheTagsInvalidator, che per ogni chiamata a invalidateTags() scrive una riga con i tag e la richiesta che li ha invalidati. Salvando un nodo dal form, lo stesso POST produce queste righe:

tag_count  tags
1          node:XXX:revisions
4          node_list, node_list:article, 4xx-response, node:XXX
1          native_observability_trace:list

La seconda riga è quella che spiega perché le liste di articoli e la pagina del nodo sono state ricostruite alla visita successiva. La terza è del modulo stesso, e se ne parla fra i limiti.

Le invalidazioni hanno un tetto per richiesta, max_stored_invalidations_per_request, 50 di default. Il tetto conta le chiamate, non i tag, e tiene le prime: l'invalidazione avviene sempre, si ferma solo la registrazione. Lo stesso salvataggio con il tetto a 1 e a 50:

Tetto Righe salvate Tag registrati Che cosa resta
1 1 1 node:XXX:revisions
50 3 6 tutte e tre le chiamate

Con il tetto a 1 sono spariti proprio node:XXX e node_list, cioè i tag utili.

Il totale vero non viene registrato da nessuna parte. Per le query lente il modulo conserva anche quante ne ha viste, e il confronto fra righe salvate e righe viste prova la troncatura. Per le invalidazioni quel confronto manca. L'unico indizio è una richiesta con esattamente tante righe quante ne consente il tetto, e la prova si ottiene soltanto rifacendo la misura con un tetto più alto. È un limite di Cache Observer, e lo scrivo qui invece di lasciarlo scoprire a chi ci si affida.

I limiti di Cache Observer

Ognuno è stato osservato nella stessa misura.

  • Un POST lascia solo la riga di cacheabilità. Il core mette X-Drupal-Cache soltanto sulle

richieste con metodo cacheabile, come prescrive la RFC 7231 §4.2.3, e senza intestazione il middleware non scrive la riga della page cache.

  • CACHEABLE è la cacheabilità dichiarata. L'etichetta della riga di cacheabilità guarda

soltanto tag, contesti e max-age. Un POST sul form di login esce CACHEABLE, benché un POST resti sempre fuori dalla cache. L'esito vero sta nelle righe response_cache.

  • Il HIT ha il percorso e la rotta vuota, per il motivo spiegato sopra.
  • Le invalidazioni da drush e da cron lanciato da riga di comando restano fuori. L'invalidatore

scrive solo quando la richiesta corrente ha un identificativo di correlazione, e sotto drush quella richiesta è sintetica e l'identificativo manca. Un drush cr svuota tutto e lascia la tabella com'era.

  • Il modulo registra le proprie invalidazioni. Ogni richiesta che scrive una traccia invalida

native_observability_trace:list, e quella chiamata finisce in tabella come le altre: nella misura erano 8 righe di invalidazione su 12. Con un tetto basso occupano posti che spetterebbero ai tag del sito.

  • Il tetto sulle invalidazioni taglia in silenzio, come descritto nella sezione precedente.

Confronto con gli altri strumenti

Ognuno risponde a una domanda diversa. La tabella riporta quello che ogni progetto dichiara sulla sua pagina: gli altri strumenti non sono stati provati nell'ambiente di misura.

Strumento A quale domanda risponde Per la produzione
debug_cacheability_headers di core quali tag, contesti e max-age ha la risposta che sto guardando sconsigliato dal core
Log Cache Tags quali tag sono stati invalidati, scritti nel dblog nessuna avvertenza, con un interruttore per il volume
Trace Cache Tags quali tag sono stati invalidati, un avviso a ogni invalidazione «Not recommended for production sites»
Cache review come funzionano page cache e dynamic page cache, con pagine dimostrative strumento didattico, lo dichiara
WebProfiler che cosa ha fatto la cache nella pagina che ho davanti nessuna indicazione
Native Observability che cosa ha fatto ogni strato di cache per una richiesta passata, e chi ha invalidato che cosa nato per restare acceso

I debug header e WebProfiler servono quando la pagina è davanti a te. I moduli di log sulle invalidazioni servono quando la domanda è solo «chi ha svuotato la cache». Nessuno dei primi cinque registra i HIT della page cache, che sono proprio le richieste di cui Drupal non sa niente.

Come si comincia

  1. Installa il sottomodulo: drush en native_observability_cache_observer -y. Richiede

native_observability e funziona su Drupal 10 e 11.

  1. Dai il permesso access native observability cache observer a chi deve leggere i dati, e

administer native observability cache observer a chi deve cambiare le impostazioni o cancellare le righe.

  1. Controlla le impostazioni su /admin/config/development/native-observability/settings/cache-observer.

Tutte le catture sono accese per default, il tetto sulle invalidazioni è 50 e le righe restano 72 ore.

  1. Verifica con due richieste anonime alla stessa pagina: la seconda deve rispondere

X-Drupal-Cache: HIT con un X-Native-Observability-Request-Id diverso dalla prima.

Il rapporto si legge su /admin/reports/native-observability/cache-observer, filtrabile per strato e per stato, con il dettaglio di ogni evento e il suo payload.

Le impostazioni stanno nell'oggetto di configurazione native_observability_cache_observer.settings:

drush cget native_observability_cache_observer.settings
drush cset native_observability_cache_observer.settings max_stored_invalidations_per_request 50 -y

Come rifare la misura

Le due richieste della page cache, da anonimo e senza cookie:

URL=https://example.ddev.site/some-published-page
curl -sk -D a1.txt -o /dev/null "$URL"
curl -sk -D a2.txt -o /dev/null "$URL"
grep -iE 'x-drupal-cache:|x-drupal-dynamic-cache:|x-native-observability-request-id' a1.txt a2.txt

La query che lega ogni evento della page cache alla sua traccia, se esiste:

SELECT c.id, c.request_id, c.status, c.path, c.route_name,
       (SELECT COUNT(*) FROM native_observability_trace t
         WHERE t.request_id = c.request_id) AS trace_rows
FROM native_observability_cache_event c
WHERE c.cache_layer = 'page_cache'
ORDER BY c.id;

Le invalidazioni salvate per ogni richiesta, da confrontare con il tetto configurato:

SELECT request_id, method, route_name,
       COUNT(*) AS stored_rows, SUM(tag_count) AS stored_tags
FROM native_observability_cache_event
WHERE event_type = 'tag_invalidation'
GROUP BY request_id, method, route_name
ORDER BY MIN(id);

Per il salvataggio del nodo serve una richiesta HTTP vera, fatta dal form: un salvataggio lanciato con drush resterebbe fuori, per il motivo scritto fra i limiti.

Che cosa questa prova non dimostra

  • È una misura fatta a mano, una richiesta alla volta, in un ambiente DDEV. Mostra il meccanismo, non

il comportamento con il traffico di un sito vero.

  • L'ambiente di misura gira in modalità sviluppo. Per la prova ho spento a mano

debug_cacheability_headers e il debug di Twig, e li ho riaccesi alla fine.

  • Le tabelle del modulo avevano già righe di prove precedenti e non sono state svuotate. Tutti i

conteggi sono filtrati sulle righe scritte dopo l'inizio della prova.

Fonti e riferimenti

Tutte le fonti sono state consultate il 27 settembre 2026.

  1. Pagina di progetto di Native Observability (si apre in una nuova scheda). Ramo 2.0.x, sottomoduli e requisiti. Fonte primaria.
  2. Sorgente del modulo, ramo 2.0.x (si apre in una nuova scheda). CachePageObserverMiddleware per i HIT e l'identificativo, CacheResponseObserverSubscriber per la cacheabilità e la dynamic page cache, ObservedCacheTagsInvalidator per le invalidazioni e il tetto, config/install per i valori di default. Fonte primaria.
  3. Sorgente del core di Drupal 11: PageCache per le etichette UNCACHEABLE della page cache e la regola sui metodi non cacheabili, DynamicPageCacheSubscriber::shouldCacheResponse() per POOR CACHEABILITY, default.services.yml per l'avvertenza sui debug header. Fonte primaria.
  4. Improve X-Drupal-Cache and X-Drupal-Dynamic-Cache headers, even for responses that are not cacheable (si apre in una nuova scheda), issue di core #2951814, aperta. Letta dal DOM reso.
  5. Cache tags, guida di Drupal (si apre in una nuova scheda). Letta dal DOM reso.
  6. Pagina di progetto di Log Cache Tags (si apre in una nuova scheda). Letta dal DOM reso.
  7. Pagina di progetto di Trace Cache Tags (si apre in una nuova scheda). Letta dal DOM reso.
  8. Pagina di progetto di Cache review (si apre in una nuova scheda). Letta dal DOM reso.
  9. Pagina di progetto di WebProfiler (si apre in una nuova scheda). Letta dal DOM reso.
  10. I numeri di questa pagina vengono da un ambiente di misura che ho costruito io, con Drupal 11.4.5 in DDEV e Native Observability 2.0.x. Chi vuole controllarli li rifà con i comandi e le query riportati sopra.
Giorgio Alfredo Pagano
Modificato dall'AI

Questo contenuto è stato prodotto dall'AI e rivisto da una persona.

Come è stata usata l'AI?

Le bozze sono prodotte con l'assistenza dell'AI, poi dirette, rivedute e verificate da una persona. Numeri, date e versioni sono confrontati con le fonti pubbliche prima della pubblicazione, e la data di quel controllo è indicata nel testo.