⏲️ Estimated reading time: 23 min
Table of Contents
In this complete guide, you’ll learn what WordPress transients are, why your database gets bloated, when it’s safe to delete only expired entries, and when a full cleanup makes sense. You’ll also get a ready-to-use plugin with a Tools page, daily cron, and an optional WP-CLI command.
A complete plugin you can drop into WordPress
You’ll get a plugin that adds Tools → Transient Cleaner, lets you clean expired transients (safe) or all transients (can cause a rebuild spike), includes proper security checks (capability + nonce), runs a daily WP-Cron job, and optionally adds a WP-CLI command.
A clear “why” and “when” explanation
This is not just code. You’ll understand what happens behind the scenes: why transients pile up, why they sometimes don’t clean themselves, how Redis changes the picture, and how to avoid CPU or DB spikes after a full cleanup.
What are WordPress transients
The simple definition
Transients are short-term cached values WordPress stores so it doesn’t repeat the same expensive work over and over. Instead of recalculating a result every time, WordPress saves it for a limited period.
Why transients exist
WordPress core and plugins use transients for:
Caching slow queries
A common example: a plugin calls an external API. Without caching, you hit that API on every page load. With a transient, you store the response for 10 minutes and the site stays fast.
Caching heavy calculations
Another example: a “Top 10 posts” widget with many conditions. It can cache the results and refresh them periodically rather than on every visitor request.
Caching both admin and front-end data
Transients are not only for visitors. Many plugins cache dashboard data, reports, lists, and statistics.
Where transients are stored and why they bloat your database
WordPress without persistent object cache
In the default setup, transients are stored in the wp_options table as:
The value key
_transient_{name}
The timeout key
_transient_timeout_{name}
The timeout is a timestamp. When it expires, WordPress should ignore it or remove it.
WordPress with Redis or Memcached
With persistent object caching, many transients live in memory instead of the database. That’s good, but it does not always mean your DB stays clean because:
Some transients still end up in the DB
Not everything moves to external cache. It depends on your drop-in, configuration, plugins, and special cases.
Site transients behave differently on multisite
In multisite, “site transients” can be stored in wp_sitemeta instead of wp_options.
Why expired transients remain in the database
WordPress does not run constant guaranteed garbage collection
WordPress does not run a dedicated always-on job that removes expired transients continuously. Cleanup happens only in certain situations.
Low traffic means cleanup triggers less often
If your site gets little traffic, the actions that would trigger cleanup may not run often enough. That’s how thousands of expired _transient_timeout_* rows accumulate.
Plugins can create transients aggressively
WooCommerce, SEO plugins, analytics plugins, import tools, and caching plugins can create a large number of transients. Some have short TTLs, others long TTLs.
Hosting migrations and restores can leave leftovers
After a migration, restore, or large import, you may end up with stale or orphaned transient rows, especially if URLs, domains, or plugin setups changed.
When it’s safe to delete expired transients vs when deleting all is risky
Cleaning “expired” is almost always safe
If a transient is expired, WordPress should not rely on it anymore. Deleting expired entries helps with:
A smaller wp_options table
Fewer rows means faster queries, smaller backups, and fewer problems with table growth.
Removing junk after updates
Core and plugins can leave old transient rows behind after updates. Cleaning expired entries keeps things tidy.
Deleting “all transients” can cause a temporary load spike
If you delete everything:
All caches rebuild at the same time
Visitors and background tasks force regeneration. CPU, DB load, and PHP worker usage can jump temporarily.
WooCommerce sites can feel it more
WooCommerce relies on cached fragments and query caches. A full purge on a busy store can increase load until caches rebuild.
The site recovers, but timing matters
If you need “all,” do it during low traffic hours.
The “Transient Cleaner (Simple)” plugin overview
What the plugin does
This plugin:
Adds Tools → Transient Cleaner
A simple Tools page in wp-admin.
Offers two cleanup modes
Expired (recommended) and All (with confirmation).
Uses proper security
manage_options capability checks and a nonce.
Runs daily automatically
WP-Cron daily job, default: clean expired only.
Adds optional WP-CLI support
wp transient-cleaner expired|all
Full WordPress plugin code: Transient Cleaner (Simple)
The plugin main file
<?php
/**
* Plugin Name: Transient Cleaner (Optimized)
* Plugin URI: https://helpzone.blog/transient-cleaner-plugin-clean-wordpress-cache-safely/
* Description: Clean expired or all WordPress transients from DB. Tools page, daily cron, WP-CLI, batch processing, and external object cache awareness.
* Author: Tokyo Blade
* Author URI: https://helpzone.blog/
* Version: 1.0.0
* License: GPL-2.0-or-later
* Text Domain: tc-transient-cleaner
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
final class TC_Transient_Cleaner
{
private const CRON_HOOK = 'tc_transient_cleaner_daily';
private const NONCE_ACTION = 'tc_transient_cleaner_action';
private const BATCH_SIZE = 500; // safer default
private const LOCK_KEY = 'tc_cleaner_lock';
private const LOCK_TTL = 300; // seconds
public static function init(): void
{
add_action('plugins_loaded', [__CLASS__, 'load_textdomain']);
add_action('admin_menu', [__CLASS__, 'register_tools_page']);
add_action('admin_post_tc_transient_cleaner', [__CLASS__, 'handle_admin_post']);
add_action('admin_notices', [__CLASS__, 'maybe_show_cache_notice']);
add_action(self::CRON_HOOK, [__CLASS__, 'cron_clean_expired']);
add_filter('cron_schedules', [__CLASS__, 'add_cron_intervals']);
register_activation_hook(__FILE__, [__CLASS__, 'activate']);
register_deactivation_hook(__FILE__, [__CLASS__, 'deactivate']);
if (defined('WP_CLI') && WP_CLI) {
self::register_wp_cli();
}
}
public static function load_textdomain(): void
{
load_plugin_textdomain(
'tc-transient-cleaner',
false,
dirname(plugin_basename(__FILE__)) . '/languages/'
);
}
public static function activate(): void
{
self::schedule_cron();
}
public static function deactivate(): void
{
self::unschedule_cron();
}
private static function schedule_cron(): void
{
if (wp_next_scheduled(self::CRON_HOOK)) {
return;
}
$timestamp = strtotime('tomorrow 3:00am', (int) current_time('timestamp'));
if (!$timestamp) {
$timestamp = time() + HOUR_IN_SECONDS;
}
wp_schedule_event($timestamp, 'daily', self::CRON_HOOK);
}
private static function unschedule_cron(): void
{
$timestamp = wp_next_scheduled(self::CRON_HOOK);
if ($timestamp) {
wp_unschedule_event($timestamp, self::CRON_HOOK);
}
}
public static function add_cron_intervals(array $schedules): array
{
if (!isset($schedules['weekly'])) {
$schedules['weekly'] = [
'interval' => WEEK_IN_SECONDS,
'display' => __('Once Weekly', 'tc-transient-cleaner'),
];
}
return $schedules;
}
/**
* Excluded transient "base keys" (without any prefix).
* Filter: tc_excluded_transient_keys
*/
private static function excluded_keys(): array
{
$keys = (array) apply_filters('tc_excluded_transient_keys', [
'update_plugins',
'update_themes',
'update_core',
]);
$keys = array_map('strval', $keys);
$keys = array_map('trim', $keys);
$keys = array_filter($keys, static fn($k) => $k !== '');
// Normalize: strip possible prefixes user might pass.
$keys = array_map([__CLASS__, 'normalize_transient_key'], $keys);
return array_values(array_unique($keys));
}
private static function normalize_transient_key(string $key): string
{
$prefixes = [
'_transient_timeout_',
'_transient_',
'_site_transient_timeout_',
'_site_transient_',
];
foreach ($prefixes as $p) {
if (str_starts_with($key, $p)) {
return substr($key, strlen($p));
}
}
return $key;
}
private static function is_excluded(string $base_key): bool
{
static $cache = null;
if ($cache === null) {
$cache = array_flip(self::excluded_keys());
}
return isset($cache[$base_key]);
}
public static function register_tools_page(): void
{
add_management_page(
__('Transient Cleaner', 'tc-transient-cleaner'),
__('Transient Cleaner', 'tc-transient-cleaner'),
'manage_options',
'tc-transient-cleaner',
[__CLASS__, 'render_tools_page']
);
}
public static function maybe_show_cache_notice(): void
{
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->id !== 'tools_page_tc-transient-cleaner') {
return;
}
if (!wp_using_ext_object_cache()) {
return;
}
echo '<div class="notice notice-warning is-dismissible"><p>';
echo esc_html__(
'⚠️ External object cache (Redis/Memcached) detected. DB cleaning may have limited effect because many transients can live in memory.',
'tc-transient-cleaner'
);
echo '</p></div>';
}
public static function render_tools_page(): void
{
if (!current_user_can('manage_options')) {
wp_die(__('You do not have permission to access this page.', 'tc-transient-cleaner'));
}
$status = isset($_GET['tc_status']) ? sanitize_text_field(wp_unslash($_GET['tc_status'])) : '';
$count = isset($_GET['tc_count']) ? (int) $_GET['tc_count'] : 0;
$mode = isset($_GET['tc_mode']) ? sanitize_text_field(wp_unslash($_GET['tc_mode'])) : '';
echo '<div class="wrap">';
echo '<h1>' . esc_html__('Transient Cleaner', 'tc-transient-cleaner') . '</h1>';
if ($status === 'ok') {
$message = ($mode === 'all')
? sprintf(__('Deleted %d transient rows (all).', 'tc-transient-cleaner'), $count)
: sprintf(__('Deleted %d expired transient rows.', 'tc-transient-cleaner'), $count);
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($message) . '</p></div>';
} elseif ($status === 'err') {
echo '<div class="notice notice-error is-dismissible"><p>' .
esc_html__('An error occurred. Please check logs or try again.', 'tc-transient-cleaner') .
'</p></div>';
} elseif ($status === 'locked') {
echo '<div class="notice notice-warning is-dismissible"><p>' .
esc_html__('A cleaning operation is already in progress. Please wait.', 'tc-transient-cleaner') .
'</p></div>';
}
self::render_stats();
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="tc_transient_cleaner" />';
wp_nonce_field(self::NONCE_ACTION, '_tc_nonce');
echo '<p>';
echo '<button class="button button-primary" name="tc_mode" value="expired">' .
esc_html__('Clean Expired (Safe)', 'tc-transient-cleaner') . '</button> ';
echo '<button class="button" name="tc_mode" value="all" onclick="return confirm(\'' .
esc_js(__('WARNING: This deletes ALL transients and may cause temporary load spikes. Continue?', 'tc-transient-cleaner')) .
'\');">' .
esc_html__('Clean ALL (Aggressive)', 'tc-transient-cleaner') . '</button>';
echo '</p>';
echo '<hr />';
echo '<p><strong>' . esc_html__('Protected keys (excluded):', 'tc-transient-cleaner') . '</strong> ';
echo esc_html(implode(', ', self::excluded_keys()));
echo '</p>';
echo '</form>';
echo '</div>';
}
private static function render_stats(): void
{
global $wpdb;
$expired_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options}
WHERE option_name LIKE %s AND option_value < %d",
$wpdb->esc_like('_transient_timeout_') . '%',
time()
));
$total_rows = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options}
WHERE option_name LIKE %s OR option_name LIKE %s",
$wpdb->esc_like('_transient_') . '%',
$wpdb->esc_like('_transient_timeout_') . '%'
));
echo '<div class="card" style="max-width:700px;margin:20px 0;padding:20px;background:#fff;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04);">';
echo '<h2>' . esc_html__('Current Status', 'tc-transient-cleaner') . '</h2>';
echo '<table class="widefat" style="border:none;">';
echo '<tr><td>' . esc_html__('Expired timeouts in DB:', 'tc-transient-cleaner') . '</td><td><strong>' . esc_html(number_format_i18n($expired_count)) . '</strong></td></tr>';
echo '<tr><td>' . esc_html__('Total transient-related rows in DB:', 'tc-transient-cleaner') . '</td><td><strong>' . esc_html(number_format_i18n($total_rows)) . '</strong></td></tr>';
echo '</table>';
echo '</div>';
}
public static function handle_admin_post(): void
{
if (!current_user_can('manage_options')) {
wp_die(__('Unauthorized.', 'tc-transient-cleaner'));
}
check_admin_referer(self::NONCE_ACTION, '_tc_nonce');
if (!self::acquire_lock()) {
wp_safe_redirect(add_query_arg([
'page' => 'tc-transient-cleaner',
'tc_status' => 'locked',
], admin_url('tools.php')));
exit;
}
$mode = isset($_POST['tc_mode']) ? sanitize_text_field(wp_unslash($_POST['tc_mode'])) : 'expired';
try {
$deleted = ($mode === 'all')
? self::delete_all_transients_db()
: self::delete_expired_transients_db();
self::release_lock();
wp_safe_redirect(add_query_arg([
'page' => 'tc-transient-cleaner',
'tc_status' => 'ok',
'tc_count' => $deleted,
'tc_mode' => $mode,
], admin_url('tools.php')));
exit;
} catch (Throwable $e) {
self::log_error($e->getMessage());
self::release_lock();
wp_safe_redirect(add_query_arg([
'page' => 'tc-transient-cleaner',
'tc_status' => 'err',
], admin_url('tools.php')));
exit;
}
}
public static function cron_clean_expired(): void
{
if (!self::acquire_lock()) {
self::log_error('Cron skipped: lock held.');
return;
}
try {
$deleted = self::delete_expired_transients_db();
self::log_info("Cron cleaned {$deleted} expired transient rows.");
} catch (Throwable $e) {
self::log_error('Cron error: ' . $e->getMessage());
} finally {
self::release_lock();
}
}
private static function acquire_lock(): bool
{
// Prefer atomic add in object cache if available.
if (function_exists('wp_cache_add')) {
$ok = wp_cache_add(self::LOCK_KEY, time(), 'tc_transient_cleaner', self::LOCK_TTL);
if ($ok) {
return true;
}
}
// Fallback transient (not atomic).
if (get_transient(self::LOCK_KEY)) {
return false;
}
return (bool) set_transient(self::LOCK_KEY, time(), self::LOCK_TTL);
}
private static function release_lock(): void
{
if (function_exists('wp_cache_delete')) {
wp_cache_delete(self::LOCK_KEY, 'tc_transient_cleaner');
}
delete_transient(self::LOCK_KEY);
}
/**
* Delete expired transients stored in wp_options (and sitemeta on multisite).
* Returns number of deleted DB rows (best effort).
*/
public static function delete_expired_transients_db(): int
{
global $wpdb;
$now = time();
$deleted = 0;
$timeout_like = $wpdb->esc_like('_transient_timeout_') . '%';
while (true) {
$rows = $wpdb->get_col($wpdb->prepare(
"SELECT option_name
FROM {$wpdb->options}
WHERE option_name LIKE %s AND option_value < %d
LIMIT %d",
$timeout_like,
$now,
self::BATCH_SIZE
));
if (!$rows) {
break;
}
foreach ($rows as $timeout_name) {
$timeout_name = (string) $timeout_name;
$base_key = substr($timeout_name, strlen('_transient_timeout_'));
if ($base_key === '' || self::is_excluded($base_key)) {
continue;
}
// Use API first (also cleans external cache); then hard-delete DB rows if present.
delete_transient($base_key);
$deleted += self::hard_delete_options_pair($base_key);
}
if (count($rows) < self::BATCH_SIZE) {
break;
}
usleep(100000);
}
if (is_multisite()) {
$deleted += self::delete_expired_site_transients_multisite($now);
}
return $deleted;
}
/**
* Delete ALL transients stored in wp_options (and sitemeta on multisite), excluding protected keys.
*/
public static function delete_all_transients_db(): int
{
global $wpdb;
$deleted = 0;
// Iterate keys in batches via timeouts + values to avoid gigantic DELETE locks.
while (true) {
$names = $wpdb->get_col($wpdb->prepare(
"SELECT option_name
FROM {$wpdb->options}
WHERE option_name LIKE %s
LIMIT %d",
$wpdb->esc_like('_transient_timeout_') . '%',
self::BATCH_SIZE
));
if (!$names) {
break;
}
foreach ($names as $timeout_name) {
$timeout_name = (string) $timeout_name;
$base_key = substr($timeout_name, strlen('_transient_timeout_'));
if ($base_key === '' || self::is_excluded($base_key)) {
continue;
}
delete_transient($base_key);
$deleted += self::hard_delete_options_pair($base_key);
}
if (count($names) < self::BATCH_SIZE) {
break;
}
usleep(100000);
}
// Also clean any value-only stragglers without timeout.
while (true) {
$names = $wpdb->get_col($wpdb->prepare(
"SELECT option_name
FROM {$wpdb->options}
WHERE option_name LIKE %s
AND option_name NOT LIKE %s
LIMIT %d",
$wpdb->esc_like('_transient_') . '%',
$wpdb->esc_like('_transient_timeout_') . '%',
self::BATCH_SIZE
));
if (!$names) {
break;
}
foreach ($names as $value_name) {
$value_name = (string) $value_name;
$base_key = substr($value_name, strlen('_transient_'));
if ($base_key === '' || self::is_excluded($base_key)) {
continue;
}
delete_transient($base_key);
$deleted += self::hard_delete_options_pair($base_key);
}
if (count($names) < self::BATCH_SIZE) {
break;
}
usleep(100000);
}
if (is_multisite()) {
$deleted += self::delete_all_site_transients_multisite();
}
return $deleted;
}
private static function hard_delete_options_pair(string $base_key): int
{
global $wpdb;
// Delete both value + timeout. Returns affected rows (0..2).
return (int) $wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE option_name IN (%s, %s)",
'_transient_' . $base_key,
'_transient_timeout_' . $base_key
));
}
private static function delete_expired_site_transients_multisite(int $now): int
{
global $wpdb;
$deleted = 0;
$timeout_like = $wpdb->esc_like('_site_transient_timeout_') . '%';
while (true) {
$rows = $wpdb->get_col($wpdb->prepare(
"SELECT meta_key
FROM {$wpdb->sitemeta}
WHERE meta_key LIKE %s AND meta_value < %d
LIMIT %d",
$timeout_like,
$now,
self::BATCH_SIZE
));
if (!$rows) {
break;
}
foreach ($rows as $timeout_key) {
$timeout_key = (string) $timeout_key;
$base_key = substr($timeout_key, strlen('_site_transient_timeout_'));
if ($base_key === '' || self::is_excluded($base_key)) {
continue;
}
delete_site_transient($base_key);
$deleted += self::hard_delete_sitemeta_pair($base_key);
}
if (count($rows) < self::BATCH_SIZE) {
break;
}
usleep(100000);
}
return $deleted;
}
private static function delete_all_site_transients_multisite(): int
{
global $wpdb;
$deleted = 0;
while (true) {
$rows = $wpdb->get_col($wpdb->prepare(
"SELECT meta_key
FROM {$wpdb->sitemeta}
WHERE meta_key LIKE %s
LIMIT %d",
$wpdb->esc_like('_site_transient_timeout_') . '%',
self::BATCH_SIZE
));
if (!$rows) {
break;
}
foreach ($rows as $timeout_key) {
$timeout_key = (string) $timeout_key;
$base_key = substr($timeout_key, strlen('_site_transient_timeout_'));
if ($base_key === '' || self::is_excluded($base_key)) {
continue;
}
delete_site_transient($base_key);
$deleted += self::hard_delete_sitemeta_pair($base_key);
}
if (count($rows) < self::BATCH_SIZE) {
break;
}
usleep(100000);
}
// Stragglers without timeout.
while (true) {
$rows = $wpdb->get_col($wpdb->prepare(
"SELECT meta_key
FROM {$wpdb->sitemeta}
WHERE meta_key LIKE %s
AND meta_key NOT LIKE %s
LIMIT %d",
$wpdb->esc_like('_site_transient_') . '%',
$wpdb->esc_like('_site_transient_timeout_') . '%',
self::BATCH_SIZE
));
if (!$rows) {
break;
}
foreach ($rows as $value_key) {
$value_key = (string) $value_key;
$base_key = substr($value_key, strlen('_site_transient_'));
if ($base_key === '' || self::is_excluded($base_key)) {
continue;
}
delete_site_transient($base_key);
$deleted += self::hard_delete_sitemeta_pair($base_key);
}
if (count($rows) < self::BATCH_SIZE) {
break;
}
usleep(100000);
}
return $deleted;
}
private static function hard_delete_sitemeta_pair(string $base_key): int
{
global $wpdb;
return (int) $wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->sitemeta}
WHERE meta_key IN (%s, %s)",
'_site_transient_' . $base_key,
'_site_transient_timeout_' . $base_key
));
}
private static function log_error(string $message): void
{
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('TC Transient Cleaner ERROR: ' . $message);
}
do_action('tc_transient_cleaner_error', $message);
}
private static function log_info(string $message): void
{
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('TC Transient Cleaner: ' . $message);
}
do_action('tc_transient_cleaner_info', $message);
}
private static function register_wp_cli(): void
{
WP_CLI::add_command('transient-cleaner', function ($args, $assoc_args) {
$mode = $args[0] ?? 'expired';
$dry_run = \WP_CLI\Utils\get_flag_value($assoc_args, 'dry-run', false);
if (!in_array($mode, ['expired', 'all'], true)) {
WP_CLI::error("Invalid mode. Use 'expired' or 'all'.");
}
if ($dry_run) {
self::wp_cli_dry_run($mode);
return;
}
if (!self::acquire_lock()) {
WP_CLI::error('Another cleaning operation is already in progress.');
}
WP_CLI::log("Starting transient cleanup (mode: {$mode})...");
try {
$start = microtime(true);
$deleted = ($mode === 'all')
? self::delete_all_transients_db()
: self::delete_expired_transients_db();
$sec = round(microtime(true) - $start, 2);
self::release_lock();
WP_CLI::success("Deleted {$deleted} transient rows in {$sec}s.");
} catch (Throwable $e) {
self::release_lock();
WP_CLI::error($e->getMessage());
}
}, [
'shortdesc' => 'Clean expired or all transients from DB.',
'synopsis' => [
[
'type' => 'positional',
'name' => 'mode',
'description' => 'expired|all',
'optional' => true,
'default' => 'expired',
],
[
'type' => 'flag',
'name' => 'dry-run',
'description' => 'Show what would be deleted without deleting',
],
],
]);
}
private static function wp_cli_dry_run(string $mode): void
{
global $wpdb;
WP_CLI::log("DRY RUN - Mode: {$mode}");
WP_CLI::log(str_repeat('-', 40));
if ($mode === 'expired') {
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options}
WHERE option_name LIKE %s AND option_value < %d",
$wpdb->esc_like('_transient_timeout_') . '%',
time()
));
WP_CLI::log("Would delete up to {$count} expired timeout rows (plus matching value rows).");
} else {
$count1 = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like('_transient_') . '%'
));
$count2 = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like('_transient_timeout_') . '%'
));
WP_CLI::log("Would delete up to {$count1} value rows + {$count2} timeout rows (excluding protected keys).");
}
$excluded = self::excluded_keys();
if ($excluded) {
WP_CLI::log("\nProtected keys (excluded):");
foreach ($excluded as $k) {
WP_CLI::log(" - {$k}");
}
}
}
}
TC_Transient_Cleaner::init();
Download Transient Cleaner Plugin Zip File
Quick installation in WordPress
Step 1: Create the plugin folder
Create this folder:
wp-content/plugins/transient-cleaner-simple/
Step 2: Create the main plugin file
Create:
wp-content/plugins/transient-cleaner-simple/transient-cleaner-simple.php
Paste the full code from above.
Step 3: Activate the plugin
Go to WordPress Admin → Plugins and activate it.
Step 4: Open the Tools page
Go to:
Tools → Transient Cleaner
How to use the plugin correctly without surprises
Recommended mode: Clean expired transients
This is the safe option and you can run it often.
“All” mode: Clean ALL transients
This is a full reset. It can help when:
You migrated the site and cache is broken
After a domain change, restore, or host migration, a full cleanup can remove stale transient data.
You’re fighting a stubborn caching bug
Sometimes a plugin stores outdated data and won’t refresh properly. Clearing all transients can force a clean rebuild.
You have a clear reason and a good timing window
Do it when traffic is low.
What the “deleted count” really means
The count is a useful estimate
For “expired,” the plugin uses a best-effort approach. It often deletes two rows per transient (value + timeout), but not always.
“All” mode is more direct
“All” removes rows using SQL DELETE ... LIKE, so the count usually reflects exact affected rows.
Daily WP-Cron: what to expect
The plugin schedules a daily event
On activation, it schedules a daily cleanup that removes expired transients.
WP-Cron is traffic-driven
It runs when your site gets visits. On very low-traffic sites, the job may run later than expected.
A real server cron is more precise
On a VPS, you can run WP-CLI or trigger wp-cron.php from Linux cron for better reliability.
WP-CLI: run cleanup from the terminal
Clean expired
wp transient-cleaner expired
Clean all
wp transient-cleaner all
When WP-CLI is the best option
WP-CLI is great if you want:
Maintenance window control
You can run it off-peak, on your schedule.
Server automation
It’s easy to cron this on Linux.
Clear logs and output
You get a straightforward “deleted X rows” result.
Performance and safety notes before you click
Transients are not your only cache layer
You may also have:
Page cache
From a caching plugin or Cloudflare.
Object cache
Redis or Memcached.
Browser cache
Via headers.
CDN cache
Cloudflare, Bunny, and similar.
Deleting transients does not clear everything
If you expect a “full reset,” you may need to clear page cache and CDN cache as well.
Common issues after “Clean ALL” and how to avoid them
CPU spikes and more DB queries
Caches rebuild. It’s normal for load to increase temporarily.
Fix: run off-peak
Pick a low-traffic time.
Fix: warm up the cache
Open key pages manually or use a cache warmer.
Fix: use “expired” regularly, not “all”
Most sites only need expired cleanup.
Recommended “Pro” improvements you can add later
Dry-run mode
A dry-run shows how many entries would be deleted without deleting anything. It’s perfect for sensitive sites.
Prefix whitelist
You can exclude transient prefixes (for example WooCommerce-related keys) when you need extra caution.
Batch limits
On huge databases, deleting in chunks is safer than a single large operation.
Logging
Store logs in Tools or debug.log so you can track what happened.

Frequently Asked Questions
What are WordPress transients, really?
Transients are temporary cached values used to avoid repeating expensive operations. They have an expiration time, but expired entries can still remain in the database.
Is it safe to delete expired transients?
Yes, almost always. If a transient is expired, WordPress should not depend on it. Removing expired entries mainly improves database hygiene.
Why can deleting all transients cause load spikes?
Because WordPress and plugins rebuild caches. If many requests hit the site during rebuild, CPU and database load can increase temporarily.
Will this plugin work with Redis object cache?
Yes. It cleans transients stored in the database. With Redis, many transients may not be in DB, but DB leftovers can still be cleaned.
Does the plugin support multisite?
Yes. It also cleans site transients from the sitemeta table when multisite is enabled.
How often should I run expired cleanup?
Daily is fine for most sites. Weekly can be enough on smaller sites. If your DB grows quickly, daily helps keep it clean.
Should I run “Clean ALL transients” regularly?
No. Use it only when you have a clear reason and ideally during a maintenance window.
Can I run it automatically with WP-CLI?
Yes. Use wp transient-cleaner expired or wp transient-cleaner all if WP-CLI is available.
Will this fix a slow WordPress admin?
Sometimes. If wp_options is bloated, cleaning expired transients can help. But admin slowness can also come from autoloaded options, slow queries, or heavy plugins.
Does the plugin delete anything else?
No. It targets only transient-related keys: _transient_*, _transient_timeout_*, and on multisite _site_transient_*, _site_transient_timeout_*.
The one key takeaway worth remembering
Transients help performance, but expired ones become database junk. Cleaning expired transients regularly is safe and smart. Cleaning all transients is a powerful reset, but you should only do it with a clear reason and good timing.
⚠️ Disclaimer and Source Hygiene
This post is for informational and educational purposes only and is not professional server, security, or database administration advice. Always test on staging first and create a backup before making database changes. The information is based on widely used WordPress practices and generally accepted documentation.
🔔 For more tutorials like this, consider subscribing to our blog.
📩 Do you have questions or suggestions? Leave a comment or contact us!
🏷️ Tags: WordPress transients, transient cleaner, wp_options cleanup, WordPress database optimization, WordPress performance, WP-Cron, WP-CLI, cache management, Redis WordPress, multisite transients
📢 Hashtags: #WordPress #WordPressTips #WPPlugins #SiteSpeed #DatabaseOptimization #Caching #WPCLI #WPCron #WebPerformance #DevOps
📚 Sources and References
Relevant WordPress documentation topics
WordPress Transients API, delete_transient, delete_site_transient, WP-Cron scheduling, WP-CLI command registration, wp_options table behavior.
Practical observations from hosting, migrations, and caching
In real WordPress operations, “expired” doesn’t always mean “removed immediately,” especially on low-traffic sites and on setups with many plugins generating transients. Best practice is regular expired cleanup and using full purge only during planned maintenance windows.