The WordPress Transients API provides a standardized interface for storing arbitrary cached data with an expiration time — it abstracts over the underlying storage mechanism so that the same set_transient() / get_transient() calls use the wp_options table on a default installation and an in-memory store (Memcached, Redis) when an object cache is active, without requiring any code changes. A transient is identified by a string key (maximum 172 characters), stores a PHP value (serialized automatically), and expires after a specified number of seconds — expired transients are deleted on the next read attempt rather than by a background process, so an occasional stale read triggering a cache miss and regeneration is the normal eviction path. The typical usage pattern is the cache-aside pattern: get_transient() returns false on a miss or expiry, the calling code regenerates the expensive data, and set_transient() stores it with a new TTL. Common WordPress candidates for transients are: remote API responses (weather widgets, social media counts, currency exchange rates), complex WP_Query results that aggregate data across many posts or use meta_query joins, computed statistics dashboards, and custom search index results. Site transients (set_site_transient()) are network-wide on Multisite and are appropriate for data shared across all sites in a network. Cache invalidation is the hardest problem: transients should be invalidated proactively when their source data changes — for example, a transient caching a post’s computed reading time should be deleted when the post is updated via the save_post hook, ensuring the next page load recalculates and stores a fresh value. Transient keys should be namespaced (e.g., myplugin_top_posts_v2_30d) to avoid collisions with other plugins and to allow targeted bulk deletion with a LIKE query when a schema or calculation changes. The Redis object caching post explains how enabling a persistent object cache upgrades transients from database-backed storage to in-memory storage automatically.
Problem: A WordPress dashboard widget queries the GitHub API to display contributor statistics for a plugin repository — each admin page load fires an HTTP request to the GitHub API, causing 500ms+ load times, rate-limit errors after a few refreshes, and broken widgets when the API is unreachable.
Solution: Wrap the API call in the Transients API cache-aside pattern with a 1-hour TTL, proactively invalidate the transient via a scheduled event that pre-warms the cache, and store the last successful response as a fallback for when the API is down.
define( 'MYPLUGIN_GH_STATS_KEY', 'myplugin_gh_stats_v1' );
define( 'MYPLUGIN_GH_STATS_TTL', HOUR_IN_SECONDS );
/**
* Get GitHub contributor stats, served from transient cache when available.
*
* @param string $repo e.g. "owner/repo-name"
* @return array|false Parsed stats array or false on unrecoverable failure.
*/
function myplugin_get_github_stats( string $repo ): array|false {
$cache_key = MYPLUGIN_GH_STATS_KEY . '_' . sanitize_key( $repo );
// 1. Return cached value if available
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
// 2. Fetch fresh data from the GitHub API
$url = 'https://api.github.com/repos/' . rawurlencode( $repo ) . '/contributors';
$response = wp_remote_get( $url, [
'timeout' => 5,
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'WordPress/' . get_bloginfo('version'),
],
] );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
// 3. On failure, return stale data stored as fallback (no TTL = permanent)
$fallback = get_option( $cache_key . '_fallback' );
return $fallback ?: false;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $body ) ) {
return false;
}
// Sanitize before storing
$stats = array_map( function( $contributor ) {
return [
'login' => sanitize_text_field( $contributor['login'] ?? '' ),
'contributions' => absint( $contributor['contributions'] ?? 0 ),
'avatar_url' => esc_url_raw( $contributor['avatar_url'] ?? '' ),
];
}, array_slice( $body, 0, 10 ) );
// 4. Cache for 1 hour and save as fallback
set_transient( $cache_key, $stats, MYPLUGIN_GH_STATS_TTL );
update_option( $cache_key . '_fallback', $stats, false ); // autoload=false
return $stats;
}
// ── Proactive cache invalidation on post save ───────────────────────────────
add_action( 'save_post', function( int $post_id ): void {
// Bust a post-specific transient when post content changes
delete_transient( 'myplugin_reading_time_' . $post_id );
} );
// ── Scheduled pre-warming to avoid cold-cache spikes ───────────────────────
add_action( 'init', function(): void {
if ( ! wp_next_scheduled( 'myplugin_prewarm_github_cache' ) ) {
wp_schedule_event( time(), 'hourly', 'myplugin_prewarm_github_cache' );
}
} );
add_action( 'myplugin_prewarm_github_cache', function(): void {
delete_transient( MYPLUGIN_GH_STATS_KEY . '_owner_my-plugin' );
myplugin_get_github_stats( 'owner/my-plugin' );
} );
// ── Bulk-delete all plugin transients on plugin update ─────────────────────
register_activation_hook( __FILE__, function(): void {
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '\_transient\_myplugin\_%'
OR option_name LIKE '\_transient\_timeout\_myplugin\_%'"
);
} );
NOTE: Transients stored in the wp_options table with autoload='yes' (the default for options, but NOT for transients — WordPress sets autoload to no for transients automatically) do not affect the initial options load. However, the stale transients accumulate in the table if a persistent object cache is not active and WordPress’s pseudo-cron cleanup misses them. Run DELETE FROM wp_options WHERE option_name LIKE '_transient_timeout_%' AND option_value < UNIX_TIMESTAMP() periodically, or use WP-CLI’s wp transient delete --expired command to keep the options table lean.