← Back to notes
snippet 1 min read PHP 7.4+ WP 5.8+

Auto-Delete Transient Rows and Orphaned Revisions via WP-Cron

Schedule a bounded WordPress maintenance task that removes expired transients and revisions whose parent post no longer exists.

Updated Sep 24, 2026 #wordpress #wp-cron #database #maintenance #php #snippet

Expired transients and orphaned revisions can accumulate in the WordPress database. This snippet schedules a daily cleanup, removes expired transient pairs, and permanently deletes revisions whose parent post has already been removed.

Add it to a small maintenance plugin or an MU plugin. A plugin is preferable to a theme because scheduled maintenance should continue when the active theme changes.

The snippet

wp-content/mu-plugins/site-maintenance.php
<?php
add_action( 'init', function() {
if ( ! wp_next_scheduled( 'my_site_daily_database_cleanup' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'my_site_daily_database_cleanup' );
}
} );
add_action( 'my_site_daily_database_cleanup', function() {
global $wpdb;
// Process a bounded batch so one cron request cannot run indefinitely.
$timeout_options = $wpdb->get_col(
$wpdb->prepare(
"SELECT option_name FROM {$wpdb->options}
WHERE option_name LIKE %s
AND CAST(option_value AS UNSIGNED) < %d
LIMIT 100",
'_transient_timeout_%',
time()
)
);
foreach ( $timeout_options as $timeout_option ) {
$key = substr( $timeout_option, strlen( '_transient_timeout_' ) );
delete_option( $timeout_option );
delete_option( '_transient_' . $key );
}
$orphaned_revisions = $wpdb->get_col(
"SELECT revision.ID
FROM {$wpdb->posts} AS revision
LEFT JOIN {$wpdb->posts} AS parent ON parent.ID = revision.post_parent
WHERE revision.post_type = 'revision'
AND parent.ID IS NULL
LIMIT 100"
);
foreach ( $orphaned_revisions as $revision_id ) {
wp_delete_post( (int) $revision_id, true );
}
} );

Why the cleanup is bounded

The query handles up to 100 expired transient pairs and 100 orphaned revisions per run. If a site has a large backlog, the daily event gradually clears it without turning one cron request into a long-running database operation. Increase the batch only after checking database load.

This targets the single-site options table. On multisite, each site has its own options table and the cleanup must run in each site context. Some plugins also manage their own cache records, so do not delete rows based only on a prefix you have not inspected.

WP-Cron depends on site traffic unless a real server cron invokes wp-cron.php. The WordPress Cron documentation explains scheduling, and WP-CLI’s cron commands can be used to inspect or trigger the event. For a terminal-based alternative, see how to create a custom WP-CLI command.