Transient Cleaner Plugin – Clean WordPress Cache Safely

⏲️ 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

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.


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.


Transient Cleaner Plugin

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.

Report an issue (max 5 words):

We store the message, post link, time, and IP (for abuse prevention). No account required.

Want to support us? Let friends in on the secret and share your favorite post!

0 online now

Live Referrers

No external referrers recorded for this post.

Photo of author

Flo

Transient Cleaner Plugin – Clean WordPress Cache Safely

Published

Update

Welcome to HelpZone.blog, your go-to hub for expert insights, practical tips, and in-depth guides across technology, lifestyle, business, entertainment, and more! Our team of passionate writers and industry experts is dedicated to bringing you the latest trends, how-to tutorials, and valuable advice to enhance your daily life. Whether you're exploring WordPress tricks, gaming insights, travel hacks, or investment strategies, HelpZone is here to empower you with knowledge. Stay informed, stay inspired because learning never stops! 🚀

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.