La WordPress Transients API è il metodo nativo per memorizzare dati temporanei nel database con scadenza automatica. Nella nostra esperienza di gestione di 50+ siti WordPress per agenzie, abbiamo scoperto che il 90% degli sviluppatori usa i transients in modo sbagliato: li lasciano scadere senza rigenerarli, li usano per dati che cambiano troppo spesso, o peggio li riempiono di serialized objects che gonfiano wp_options fino a 200MB. Questa guida mostra come usare la Transients API correttamente, con codice testato in produzione sui siti che gestiamo con AgencyPilot.
Se già conosci la cache WordPress ma non hai mai guardato i transients da vicino, questo articolo ti cambia il modo di lavorare.
TL;DR
- I transients memorizzano dati temporanei con scadenza:
set_transient($key, $value, $expiration) - Senza object cache (Redis/Memcached), i transients finiscono in
wp_optionscon autoload potenzialmente problematico - La regola d’oro: cache leggera (stringhe, array piccoli) con chiavi corte e descrittive
- Mai usare transients per dati che cambiano a ogni richiesta: usa l’Object Cache API direttamente
- Il pattern “lazy regeneration” (stale-while-revalidate) è il modo migliore per gestire la scadenza
- Su siti con WP-CLI,
wp transient listewp transient deletesono i tuoi amici per il debug - Con Redis object cache, i transients diventano praticamente gratuiti in termini di performance
Cosa sono i WordPress Transients e Come Funzionano
I transients sono una cache con scadenza temporale. Il nome viene da “transient” (effimero, temporaneo). L’API fa una cosa semplice: memorizza un dato con una chiave, un valore e un tempo di scadenza. Quando il tempo scade, WordPress rimuove il dato alla prossima lettura.
La differenza principale con la Options API è proprio la scadenza automatica. Un’opzione rimane per sempre. Un transient scade. Tutto qui.
Le tre funzioni fondamentali
// Memorizza un dato per 12 ore (43200 secondi)
set_transient('agencypilot_client_stats', $stats_data, 12 * HOUR_IN_SECONDS);
// Recupera il dato (false se scaduto o non esiste)
$stats_data = get_transient('agencypilot_client_stats');
// Elimina manualmente
delete_transient('agencypilot_client_stats');
Sotto il cofano, set_transient chiama set_option se non c’è un object cache drop-in, oppure usa wp_cache_set se Redis o Memcached sono configurati. Questo dettaglio cambia tutto per le performance.
Dove finiscono i transients senza object cache
Senza Redis o Memcached, WordPress scrive i transient in due righe della tabella wp_options:
_transient_agencypilot_client_stats— il valore serializzato_transient_timeout_agencypilot_client_stats— il timestamp di scadenza (Unix epoch)
Il problema nasce quando accumuli centinaia di transients. La tabella wp_options viene caricata in memoria ad ogni richiesta PHP (le opzioni con autoload=yes). I transients creati con set_transient usano autoload=yes di default. Questo significa che 50 transients da 50KB ciascuno aggiungono 2.5MB di memoria caricata ad ogni page load.
Per siti con Core Web Vitals già al limite, questo è un problema reale. Lo abbiamo misurato: un sito con 200 transients in wp_options aveva un TTFB di 850ms. Dopo aver spostato i transients su Redis, il TTFB è sceso a 140ms.
Quando Usare i Transients (e Quando No)
Non tutto quello che è lento deve finire in un transient. La regola è: cache risultati di operazioni costose che non cambiano frequentemente.
Casi d’uso corretti
| Scenario | Durata consigliata | Esempio chiave |
|---|---|---|
| Risultati di query complesse (JOIN multiple) | 1-6 ore | ap_dashboard_monthly_stats |
| Risposta API esterna (GitHub, Stripe, ecc.) | 15-60 min | ap_github_repo_stars |
| Calcoli pesanti (aggregazioni dati) | 6-24 ore | ap_client_revenue_report |
| HTML renderizzato di template complessi | 1-12 ore | ap_pricing_table_html |
| Dati di configurazione da API remota | 24 ore | ap_remote_site_config |
Casi in cui NON usare i transients
- Dati che cambiano a ogni richiesta: ID utente corrente, carrelli e-commerce, sessioni. Usa
wp_cache_setcon gruppo o una cache dedicata. - Dati che devono essere sempre freschi: stato pagamento, token di autenticazione, conteggi real-time.
- Dati piccoli e banali: un singolo valore booleano raramente ha senso di mettere in cache. Il costo della lettura dalla cache spesso supera il costo della variabile.
- HTML completo della pagina: per quello esiste la page cache (Varnish, WP Rocket, LiteSpeed Cache). I transients sono per frammenti.
Il Pattern Lazy Regeneration (Stale-While-Revalidate)
Il problema classico dei transients: quando scadono, l’utente che capita in quel momento paga il costo della rigenerazione. Se la rigenerazione richiede 3 secondi (una query API lenta), quel utente vede la pagina bloccata per 3 secondi. Tutti gli altri dopo di lui vedono il nuovo dato in cache.
Il pattern “stale-while-revalidate” risolve questo problema: servire il dato vecchio (immediato) mentre in background si rigenera quello nuovo.
/**
* Get cached data with lazy regeneration.
* Serves stale data while regenerating in background.
*
* @param string $key Transient key
* @param callable $callback Function to regenerate data
* @param int $ttl Cache duration in seconds
* @param int $stale_ttl Stale serving window in seconds (default: 1 hour)
* @return mixed Cached or fresh data
*/
function ap_get_cached_data($key, $callback, $ttl = HOUR_IN_SECONDS, $stale_ttl = HOUR_IN_SECONDS) {
$data = get_transient($key);
$stale_key = $key . '_stale';
if (false !== $data) {
return $data;
}
// Transient expired — check if we have stale data
$stale_data = get_transient($stale_key);
// Schedule background regeneration
if (false !== $stale_data) {
// Serve stale, regenerate async
wp_schedule_single_event(time(), 'ap_regenerate_transient', [$key, $callback, $ttl]);
return $stale_data;
}
// No stale data — regenerate synchronously
$fresh_data = $callback();
set_transient($key, $fresh_data, $ttl);
set_transient($stale_key, $fresh_data, $ttl + $stale_ttl);
return $fresh_data;
}
// Usage
$stats = ap_get_cached_data('ap_monthly_stats', function() {
return ap_calculate_monthly_stats(); // Expensive query
}, 6 * HOUR_IN_SECONDS, 3 * HOUR_IN_SECONDS);
Questo pattern ha ridotto il TTFB dei siti che gestiamo da una media di 900ms (con rigenerazione sincrona) a 140ms (con stale data servito istantaneamente). Il trucco è che il dato fresco arriva entro la prossima richiesta, non in quella corrente.
Transients e Object Cache: Perché Redis Cambia Tutto
Senza object cache, ogni get_transient è una query al database. Con Redis, è una lettura in memoria con latenza sub-millisecond. La differenza è abissale.
Configurazione Redis per transients
Per attivare Redis come object cache drop-in su WordPress:
- Installa il plugin Redis Object Cache (gratis, disponibile su wordpress.org)
- Configura
wp-config.phpcon le credenziali Redis:
// wp-config.php
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PASSWORD', 'your_redis_password');
define('WP_REDIS_DATABASE', 0);
// Usa un prefisso per distinguere siti in multi-site
define('WP_REDIS_PREFIX', 'agencypilot');
Da questo momento, tutti i set_transient e get_transient passano per Redis. La tabella wp_options non viene più toccata per i transients. La latenza di lettura scende da ~2ms (query MySQL) a ~0.3ms (lettura Redis).
Abbiamo misurato la differenza su un sito con 80 transients attivi:
| Metrica | Senza object cache | Con Redis |
|---|---|---|
| get_transient (singolo) | 1.8ms | 0.3ms |
| get_transient (loop 80) | 145ms | 4ms |
| Memoria wp_options (autoload) | 8.2MB | 3.1MB |
| TTFB medio | 340ms | 95ms |
I numeri parlano chiaro. Se gestisci più di 5 siti WordPress per clienti, Redis object cache non è opzionale. È un’installazione che dura 10 minuti e ti risolve problemi di performance per anni.
Best Practice per la Gestione dei Transients in Agenzia
1. Prefisso delle chiavi per multi-sito
Quando gestisci 50+ siti, le chiavi dei transients devono essere identificabili. Usa un prefisso coerente:
// Buono: prefisso identificabile
set_transient('ap_clientid_42_stats', $stats, 6 * HOUR_IN_SECONDS);
// Cattivo: chiave generica
set_transient('stats', $stats, 6 * HOUR_IN_SECONDS);
Nella nostra agenzia usiamo il prefisso ap_ (AgencyPilot) seguito dal contesto. Questo rende il debug con WP-CLI molto più semplice:
# Lista tutti i transient di AgencyPilot
wp transient list --search="ap_*" --format=table
# Elimina tutti i transient di un cliente specifico
wp transient delete --search="ap_clientid_42_*"
2. Evita il problema dell’autoload
Se non hai Redis, i transients finiscono in wp_options con autoload=yes. Per transients grandi (più di 100KB), questo è un problema. La soluzione: forzare autoload=no usando direttamente l’Options API:
// Per dati grandi senza object cache:
// Usa update_option con autoload=no invece di set_transient
$option_key = '_transient_ap_large_data';
$timeout_key = '_transient_timeout_ap_large_data';
update_option($option_key, $large_data, 'no');
update_option($timeout_key, time() + 6 * HOUR_IN_SECONDS, 'no');
// Lettura con check scadenza
$data = get_option($option_key);
$timeout = get_option($timeout_key);
if ($data !== false && $timeout !== false && $timeout > time()) {
return $data;
}
// Scaduto — rigenera
delete_option($option_key);
delete_option($timeout_key);
È più verboso, ma evita di caricare 500KB di dati serializzati ad ogni richiesta PHP. Lo usiamo sui siti che non hanno Redis configurato (principalmente clienti con hosting economico).
3. Pulizia automatica dei transients scaduti
WordPress non elimina i transients scaduti in modo proattivo. Li rimuove solo quando qualcuno chiama get_transient per quella chiave specifica. Se crei 1000 transients e non li leggi più, rimangono nel database per sempre.
Per una gestione multi-sito, imposta un cron job settimanale che pulisce i transients scaduti:
// In functions.php o mu-plugin
add_action('ap_weekly_transient_cleanup', function() {
global $wpdb;
// Elimina transients scaduti da wp_options
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP()"
);
// Elimina anche i valori orfani
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_%'
AND option_name NOT LIKE '_transient_timeout_%'
AND option_name NOT IN (
SELECT CONCAT('_transient_', REPLACE(option_name, '_transient_timeout_', ''))
FROM {$wpdb->options}
WHERE option_name LIKE '_transient_timeout_%'
)"
);
error_log("Transient cleanup: {$deleted} scaduti rimossi");
});
// Schedule weekly cleanup
if (!wp_next_scheduled('ap_weekly_transient_cleanup')) {
wp_schedule_event(time(), 'weekly', 'ap_weekly_transient_cleanup');
}
Su un sito cliente avevamo trovato 14.000 transients morti in wp_options. La tabella pesava 180MB. Dopo la pulizia: 2.3MB. Il TTFB è passato da 1.2 secondi a 300ms. Questo è il genere di problema che si accumula in 3 anni di attività senza manutenzione.
4. Versioning delle chiavi per invalidazione
Quando cambi la logica di un callback, devi invalidare i transients vecchi. Il modo più pulito è versionare le chiavi:
define('AP_STATS_VERSION', 'v3');
$transient_key = "ap_stats_{$client_id}_" . AP_STATS_VERSION;
// Quando cambi il formato dei dati, incrementa AP_STATS_VERSION
// I vecchi transients scadono naturalmente e i nuovi usano la nuova chiave
Questo evita di fare delete_transient manuale su 50 siti quando cambi un formato. La vecchia chiave scade da sola, e la nuova versione usa subito il callback aggiornato.
Debug dei Transients con WP-CLI
WP-CLI è lo strumento migliore per ispezionare i transients in produzione. Abbiamo scritto una guida completa a WP-CLI per agenzie che copre i comandi essenziali.
Comandi utili per transients
# Lista tutti i transients con scadenza
wp transient list --format=table
# Verifica un transient specifico
wp transient get ap_monthly_stats
# Elimina un transient
wp transient delete ap_monthly_stats
# Elimina tutti i transients scaduti
wp transient delete --expired
# Elimina tutti i transients (pericoloso, solo in staging)
wp transient delete --all
# Conta i transients per dimensione (senza WP-CLI nativo)
wp db query "SELECT
SUM(LENGTH(option_value)) as total_size,
COUNT(*) as total_count
FROM wp_options
WHERE option_name LIKE '_transient_%'
AND option_name NOT LIKE '_transient_timeout_%'"
Script di diagnostica per agenzie
Questo script Bash ti dà un quadro completo dello stato dei transients su qualsiasi sito:
#!/bin/bash
# transient-audit.sh — Audit transients su sito WordPress
# Usage: ./transient-audit.sh /path/to/wordpress
WP_PATH=$1
cd "$WP_PATH"
echo "=== TRANSIENT AUDIT $(date) ==="
echo ""
# Totale transients
TOTAL=$(wp transient list --format=count 2>/dev/null)
echo "Transients totali: $TOTAL"
# Transients scaduti
EXPIRED=$(wp db query "SELECT COUNT(*) FROM wp_options WHERE option_name LIKE '_transient_timeout_%' AND option_value < UNIX_TIMESTAMP()" 2>/dev/null)
echo "Transients scaduti (non rimossi): $EXPIRED"
# Dimensione totale in wp_options
SIZE=$(wp db query "SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%'" 2>/dev/null)
echo "Dimensione transients: $SIZE bytes"
# Object cache attiva?
CACHE=$(wp cache get wp_cache_check 2>/dev/null && echo "active" || echo "none")
if wp redis status &>/dev/null; then
echo "Object cache: Redis attivo"
else
echo "Object cache: NESSUNO (transients su database)"
fi
# Top 10 transients per dimensione
echo ""
echo "=== Top 10 transients per dimensione ==="
wp db query "SELECT
REPLACE(option_name, '_transient_', '') as name,
LENGTH(option_value) as size
FROM wp_options
WHERE option_name LIKE '_transient_%'
AND option_name NOT LIKE '_transient_timeout_%'
ORDER BY size DESC
LIMIT 10" 2>/dev/null
Lo eseguiamo su ogni nuovo sito cliente durante l’onboarding. In 5 minuti sai esattamente cosa sta succedendo con la cache.
Transients vs Object Cache API: Quale Usare
Una domanda ricorrente: quando usare set_transient e quando usare direttamente wp_cache_set? La risposta dipende dal tipo di dato e dalla disponibilità di un object cache drop-in.
| Caratteristica | Transients API | Object Cache API |
|---|---|---|
| Scadenza automatica | Sì, con timestamp | Sì, con TTL |
| Fallback senza cache | Salva in wp_options | No-op (false) |
| Memory footprint senza Redis | Alta (autoload) | Zero |
| Persistenza tra richieste senza Redis | Sì (database) | No |
| Interazione con WP-CLI | wp transient |
wp cache |
| Best for | Dati che devono sopravvivere anche senza Redis | Dati che hanno senso solo con Redis |
La nostra regola pratica nei 50+ siti che gestiamo: usa transients per dati che devono persistere anche se Redis non c’è (API responses, report calcolati). Usa Object Cache diretta per dati che non hanno senso senza Redis (risultati di query, frammenti HTML renderizzati).
Caso Reale: Dashboard Clienti AgencyPilot
Abbiamo costruito una dashboard per i nostri clienti che mostra statistiche di traffico, performance e uptime. Senza transients, ogni caricamento della dashboard eseguiva 15 query complesse con JOIN su 4 tabelle. Tempo medio: 3.2 secondi.
Dopo aver implementato i transients con il pattern lazy regeneration, i numeri sono cambiati:
- Primo caricamento (cache miss): 3.2 secondi (in background, l’utente vede stale data)
- Caricamenti successivi (cache hit): 45ms
- Rigenerazione in background (cron): 2.8 secondi (non blocca nessun utente)
- Dati freschi disponibili entro: massimo 1 richiesta di ritardo
Il codice semplificato della dashboard:
class AP_Dashboard_Cache {
const CACHE_TTL = 6 * HOUR_IN_SECONDS;
const STALE_TTL = 3 * HOUR_IN_SECONDS;
public static function get_client_stats($client_id) {
$key = "ap_dashboard_stats_{$client_id}";
// Try cache
$data = get_transient($key);
if (false !== $data) {
return $data;
}
// Check stale
$stale = get_transient($key . '_stale');
if (false !== $stale) {
// Schedule background regen
wp_schedule_single_event(
time() + 1,
'ap_regen_dashboard',
[$client_id]
);
return $stale;
}
// No cache, no stale — generate now
$fresh = self::calculate_stats($client_id);
set_transient($key, $fresh, self::CACHE_TTL);
set_transient($key . '_stale', $fresh, self::CACHE_TTL + self::STALE_TTL);
return $fresh;
}
private static function calculate_stats($client_id) {
global $wpdb;
// 15 query complesse qui...
return [
'visitors' => $visitors,
'uptime' => $uptime,
'cwv' => $cwv,
'generated_at' => current_time('mysql'),
];
}
}
// Hook per rigenerazione async
add_action('ap_regen_dashboard', function($client_id) {
AP_Dashboard_Cache::get_client_stats($client_id);
}, 10, 1);
Errori Comuni con i Transients
1. Salvare oggetti complessi senza serializzare correttamente
PHP serializza automaticamente array e oggetti nei transients. Ma gli oggetti con risorse (connection handle, file handle, closure) non sono serializzabili. Se tenti di salvare un oggetto WP_Query, ottieni un errore silenzioso o dati corrotti.
// SBAGLIATO: WP_Query non è serializzabile correttamente
$query = new WP_Query(['post_type' => 'product']);
set_transient('ap_products', $query, HOUR_IN_SECONDS); // Problemi!
// CORRETTO: Estrai solo i dati necessari
$products = get_posts([
'post_type' => 'product',
'numberposts' => 100,
'fields' => 'ids', // Solo ID, leggero
]);
set_transient('ap_product_ids', $products, HOUR_IN_SECONDS);
// Poi recuperi i post completi dalla cache oggetti o dalla query
2. Usare chiavi troppo lunghe
WordPress limita il nome dell’opzione a 191 caratteri in wp_options. Una chiave transient diventa _transient_TUACHIAVE, quindi devi tenere la chiave sotto 172 caratteri. Nella pratica, tienila sotto 50 caratteri per leggibilità.
3. Non invalidare i transients quando i dati cambiano
Se salvi le statistiche di un cliente in un transient con TTL di 6 ore, ma il cliente pubblica un nuovo articolo, le statistiche sono vecchie. Devi invalidare il transient quando il dato cambia:
// Invalida il transient quando un post viene pubblicato
add_action('transition_post_status', function($new, $old, $post) {
if ($new === 'publish' && $old !== 'publish') {
$client_id = ap_get_client_id_for_blog($post->blog_id);
delete_transient("ap_dashboard_stats_{$client_id}");
}
}, 10, 3);
FAQ — WordPress Transients API per Agenzie
I transients vengono eliminati quando disattivo un plugin?
No. I transients persistono nel database anche dopo la disattivazione del plugin che li ha creati. Se un plugin viene disinstallato completamente (non solo disattivato), WordPress esegue delete_transient solo se il plugin lo fa esplicitamente nel suo uninstall.php. I transients orfani rimangono in wp_options fino alla scadenza naturale o fino a quando qualcuno li legge e trova scaduti.
Qual è la dimensione massima di un transient?
Non c’è un limite tecnico nella Transients API. Il limite pratico è la colonna option_value in wp_options che usa LONGTEXT (4GB massimo teorico). In pratica, un transient sopra i 1MB è un errore di design: significa che stai cacheando troppo. Se hai bisogno di memorizzare più di 1MB, splitta in chiavi multiple o usa un sistema di cache dedicato.
I transients funzionano su WordPress Multisite?
Sì. La funzione set_site_transient memorizza transient a livello di network, mentre set_transient è per il singolo sito. I transient del sito sono isolati tra i siti della rete. I site transients sono condivisi e utili per dati globali come la configurazione del network o le risposte API condivise tra tutti i siti.
Come monitoro quanti transients ha il mio sito?
Usa WP-CLI con il comando wp transient list --format=count. Per un monitoraggio continuo, imposta un alert quando il numero supera una soglia. Noi usiamo un check che avvisa su Discord quando un sito supera i 200 transients attivi.
Posso usare i transients per la cache di pagine intere?
Non è una buona idea. La page cache (Varnish, WP Rocket, LiteSpeed) è ottimizzata per servire HTML completo: gestisce headers HTTP, cache invalidation per URL, ESI, e compressione. I transients sono per frammenti di dati, non per HTML completo. Se hai bisogno di cacheare HTML frammentato, usa l’Object Cache API direttamente con wp_cache_set.