What are Transients?
WordPress transients are a simple way to cache data with an expiration time. They’re designed to store temporary data that’s expensive to
regenerate:
// Store data for 5 minutes
set_transient( 'my_cache_key', $data, 5 * MINUTE_IN_SECONDS );
// Retrieve it later
$data = get_transient( 'my_cache_key' );
On sites with a persistent object cache (Redis, Memcached), transients are stored in memory and automatically expire. But on sites without
an object cache, transients are stored in the wp_options table.
The Pitfall: Lazy Garbage Collection
Here’s what most developers don’t realize: WordPress only deletes expired transients when you call get_transient() on that specific
transient.
This works fine for static cache keys:
set_transient( 'my_plugin_settings', $settings, HOUR_IN_SECONDS );
// Later calls to get_transient( 'my_plugin_settings' ) will clean up expired data
But it becomes a problem with dynamic cache keys:
$cache_key = 'api_response_' . md5( $endpoint . $user_id . $date_range );
set_transient( $cache_key, $response, 5 * MINUTE_IN_SECONDS );
When the parameters change, you create a new transient with a different key. The old transient expires but is never accessed again, so
it’s never deleted. Over time, thousands of orphaned transients accumulate in wp_options, causing database bloat.
The Solution: Proactive Cleanup
Add a scheduled cleanup job that periodically purges expired transients with your prefix:
// Register the cron event on plugin activation
register_activation_hook( FILE, function() {
if ( ! wp_next_scheduled( 'my_plugin_cleanup_transients' ) ) {
wp_schedule_event( time(), 'daily', 'my_plugin_cleanup_transients' );
}
});
// Clean up on deactivation
register_deactivation_hook( FILE, function() {
wp_clear_scheduled_hook( 'my_plugin_cleanup_transients' );
});
// The cleanup function
add_action( 'my_plugin_cleanup_transients', function() {
global $wpdb;
// Only run cleanup if not using external object cache
if ( wp_using_ext_object_cache() ) {
return;
}
// Delete expired transients with our prefix
$wpdb->query(
$wpdb->prepare(
"DELETE a, b FROM {$wpdb->options} a
LEFT JOIN {$wpdb->options} b ON b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12))
WHERE a.option_name LIKE %s
AND b.option_value < %d",
$wpdb->esc_like( '_transient_my_plugin_cache_' ) . '%',
time()
)
);
});
Key Takeaways
- Transients with dynamic keys are dangerous on sites without persistent object cache
- Always implement cleanup if you’re creating transients with variable cache keys
- Consider alternatives like storing cache in post meta (for post-specific data) or using a bounded cache with fixed slots
- Check wp_using_ext_object_cache() to conditionally adjust your caching strategy










