How to Clean Up Expired Transients in WordPress Without Plugins (Spring Cleaning Edition)

Every March I get the same itch: open the closet, find the junk drawer, and finally toss the stuff I meant to toss months ago. On WordPress sites, that “junk drawer” in the wp_options table is often expired transients WordPress that never got cleaned up.

Most of the time, you won’t notice them, until you do. The wp_options table, also known as the options table, grows, backups get heavier, and some admin pages start to feel sticky. Cleaning up expired transients WordPress is a key database optimization step for better website performance. So in this guide I’m going to clean up expired transients WordPress style, without installing a plugin, using WP-CLI, SQL, or a tiny bit of safe code.

Why expired transients pile up (and when they matter)

Black-and-white high-contrast illustration of a WordPress admin/engineer character sweeping cobwebs and dusty cache tags like 'transient' from a database vault, featuring cleanup tools, time metaphors, and subtle code motifs.
An “expired cache” spring-cleaning scene in a database vault, created with AI.

The Transients API provides temporary storage in WordPress for cached data like external API requests. Developers use functions such as set_transient to store values with an expiration time and get_transient to retrieve them. WordPress core, themes, and plenty of plugins rely on transients too (update checks, feeds, counts, you name it).

Here’s the part that surprises people: expired transients don’t always disappear right away.

WordPress often deletes them opportunistically, meaning “when something loads and notices they’re expired.” On low-traffic sites, that can mean expired rows sit around for weeks, contributing to a bloated database. On busy sites, a persistent object cache can change where transients live, which changes what “cleanup” even means.

The database pattern is also a little odd:

  • The value is stored under an option name like _transient_some_key
  • The expiration is stored separately as _transient_timeout_some_key
  • A transient is “expired” when the timeout is less than the current Unix time

If you only delete the value and leave the timeout, you don’t really clean. If you delete both with delete_transient, you’re doing it right.

So yes, this cleanup is real, and it can help. Still, don’t expect miracles. This is maintenance, not a magic speed button.

My safety prep before I delete anything

Before I touch production, I do two things every time: I take a backup database, and I check whether I’m dealing with DB transients or an object cache.

1) Take a fast backup (pick one)

  • WP-CLI: wp db export ~/before-transients-$(date +%F).sql
  • Shell: mysqldump -u DBUSER -p DBNAME > before-transients.sql

2) Know where your transients are stored

If you use Redis or Memcached with a persistent object cache, many transients won’t be in your WordPress database wp_options table. You can still have some there, but the “big win” might be smaller than you expect. Memcached users may see different results during their check of the WordPress database.

To help decide what to use, this quick table matches the method to the situation:

MethodBest forRisk level
WP-CLIMost single sites, quick cleanupLow
SQL in phpMyAdmin/mysqlHuge wp_options, need precisionMedium
Tiny PHP snippetNo SSH, controlled one-time cleanupMedium

If you want a broader WP-CLI workflow beyond transients, I keep a handy reference of WP-CLI commands for transients management.

Method 1: Delete expired transients with WP-CLI (fastest, safest)

Black-and-white high-contrast ink and pencil crosshatching illustration of a single WordPress engineer at a desk typing a WP-CLI command to delete transients, with digital dust and expired cache tags flying from the laptop screen into a trash bin, and an hourglass tipping over nearby.
WP-CLI “trash day” for expired cache entries, created with AI.

This is my go-to routine housekeeping task because it’s hard to mess up, stays inside WordPress logic, and avoids manual database queries that might be less efficient.

First, SSH in and cd into the WordPress root (the folder with wp-config.php). Then run:

wp transient delete --expired

That’s it. WP-CLI will scan and delete only transients it considers expired.

If you want to sanity-check after, I usually run:

wp transient list --fields=transient,expiration --format=table

A few notes from real life:

  • If you’re on multisite, you may need to run it per site (because transients can be site-specific). A simple pattern is to loop through site URLs and call WP-CLI with --url=....
  • If a plugin aggressively recreates transients, they’ll come back. That’s normal. Cleanup is still useful.
  • If you’re curious why manual database queries can be tricky here, this Stack Overflow discussion on identifying expired transients shows how expiration logic depends on timeout rows.

Warning: don’t run broad “delete all transients” commands on production unless you enjoy surprise cache rebuild storms.

Method 2: Remove expired transients with SQL (phpMyAdmin or mysql)

Black-and-white high-contrast ink and pencil crosshatching illustration of a sysadmin at a computer screen displaying phpMyAdmin with an SQL query deleting expired transients from the wp_options table, crossed-out rows vanishing into dust, a nearby broom, and a sand-running clock.
SQL cleanup of expired rows inside phpMyAdmin, created with AI.

I only switch to SQL when I’m staring at a massive options table like wp_options and want a direct sweep of orphaned expiration records (while transients live here, wp_postmeta is another common source of bloat). The key is to preview first, then delete.

Step 1: Preview how many are expired

In phpMyAdmin (or mysql), run this sql query on the wp_options table. Replace wp_options with your real table name if your prefix differs:

SELECT COUNT(*) AS expired_transient_timeouts FROM wp_options WHERE option_name LIKE '_transient_timeout_%' AND option_value < UNIX_TIMESTAMP();

If that number is big, you’re on the right trail.

Step 2: Delete expired transients (value + timeout)

This sql query deletes both the timeout row and its matching value row from the wp_options table:

DELETE t, v FROM wp_options t JOIN wp_options v ON v.option_name = REPLACE(t.option_name, '_timeout_', '_') WHERE t.option_name LIKE '_transient_timeout_%' AND t.option_value < UNIX_TIMESTAMP();

After running the delete, run OPTIMIZE TABLE wp_options; to reclaim space in the options table.

If you also want to target site transients in a single site install, run the same pattern with _site_transient_timeout_:

DELETE t, v FROM wp_options t JOIN wp_options v ON v.option_name = REPLACE(t.option_name, '_timeout_', '_') WHERE t.option_name LIKE '_site_transient_timeout_%' AND t.option_value < UNIX_TIMESTAMP();

For multisite, site transients can live in wp_sitemeta, and the sql pattern changes for network-wide transients. That’s one reason I prefer WP-CLI on networks.

If you want extra background on how common this cleanup is, there’s even a dedicated tool for it, see the Delete Expired Transients plugin listing (I’m not using it here, but it’s a useful reference point). For a more opinionated take from the caching world, this WP Rocket issue thread about expired transients cleanup is also worth skimming.

A cautious “no-SSH” option: one-time cleanup snippet (then remove it)

Sometimes I’m locked out of SSH on a client host. In those cases, I add a short must-use plugin, load one admin page once, confirm it ran, then delete the file.

Create: wp-content/mu-plugins/cleanup-expired-transients.php (create the folder if needed). Paste this exact snippet, then save:

<?php add_action('admin_init', function () { if (!current_user_can('manage_options')) return; global $wpdb; $now=time(); $rows=$wpdb->get_col($wpdb->prepare("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND option_value < %d", '_transient_timeout_%', $now)); foreach ($rows as $timeout) { $key=str_replace('_transient_timeout_','',$timeout); delete_transient($key); } });

This snippet identifies expired transient records by comparing their timeout values to the current PHP time(). These transient records often have an autoload flag set to ‘yes’, which bloats the options table and increases database load. Developers might otherwise trigger a refresh or cleanup via the save_post hook in custom code.

Then load any wp-admin page once while logged in as an admin. After that, delete the file.

This approach uses delete_transient() as the core function, so WordPress handles the paired cleanup. Still, keep it temporary.

Conclusion: keep transients tidy, keep surprises low

Cleaning expired transients is like emptying the lint trap. It won’t remodel your house, but it helps your site run cleaner and supports long-term website performance through better database optimization. A leaner database improves the efficiency of the wp_query class and overall site responsiveness. Start with WP-CLI, move to SQL only when you need the extra control, and always keep a rollback nearby.

Safe execution + rollback checklist

  • Backup database first: wp db export ~/before-transients-$(date +%F).sql
  • Prefer WP-CLI: wp transient delete --expired
  • If using SQL, preview counts before deletes
  • Run deletes off-peak to avoid cache rebuild spikes
  • Rollback plan: restore the backup with wp db import /path/to/before-transients-YYYY-MM-DD.sql
  • If you used a snippet, delete the MU plugin file right after it runs

Leave a Reply

Your email address will not be published. Required fields are marked *