Clean expired transients and Action Scheduler logs in WooCommerce

Published Updated 4 min read

WooCommerce processes orders, runs background jobs and caches data across dozens of tables, and two of them grow without limit unless something trims them. wp_options fills with expired transients WordPress never cleaned up, and the Action Scheduler tables keep a row for every job, including ones that finished years ago. Neither is cleaned automatically, and both slow the admin over time.

The usual answer is a general database plugin like WP-Optimize, which sweeps expired transients, revisions, spam comments and table fragmentation in one scheduled run. That suits plenty of stores. Where the acute problem is only transient bloat and Action Scheduler clutter, it is more than the job needs.

This plugin does two things on a daily cron: it deletes expired transients from wp_options, and it removes completed and failed Action Scheduler records older than a number of days you set. WooCommerce > DB Cleanup shows the counts and a button to run it now. No table optimisation, no revision cleanup, no subscription.

Two tables that only grow

A transient is WordPress's temporary cache: two rows in wp_options, the value under _transient_X and its expiry under _transient_timeout_X. WordPress is meant to delete an expired transient the next time the key is requested, but under load that step gets skipped, or nothing ever asks for the key again, and the rows stay forever. Tens of thousands of expired rows sitting for months is not unusual on a busy store.

Action Scheduler is WooCommerce's job queue: order emails, subscription renewals, stock syncs, anything a plugin does not want to run inline. Every task is a row in wp_actionscheduler_actions, marked complete or failed when it finishes and never removed unless something deletes it. A store that has run for a few years with active plugins commonly has several hundred thousand rows, most recording jobs nobody will read again.

Why a bigger table means a slower admin

Expired transient rows are not autoloaded, so they add nothing to each request directly. They bloat wp_options until queries that scan it take longer. WordPress loads every autoloaded option in one query per request, and on a fifty-thousand-row table that query still works through a large index even when it returns two hundred rows.

Action Scheduler hurts more directly. WooCommerce and its extensions query the actions table constantly to schedule and claim jobs, and past a few hundred thousand rows those queries slow even with correct indexes. Admin pages listing scheduled actions, backup plugins that read the whole table and frequent cron runners all feel it, most visibly in the WooCommerce dashboard and WP-Cron status reports.

Two passes a day

The daily event runs two passes. The first finds rows in wp_options whose name starts with _transient_timeout_ and whose stored value is before the current Unix timestamp, and deletes each one with its matching _transient_ row. Live transients are untouched.

The second reads the retention setting (seven days by default), collects actions in wp_actionscheduler_actions that are complete, failed or canceled and older than that, and deletes their rows in wp_actionscheduler_logs before the actions themselves, so no orphaned log rows are left behind.

The admin page shows the count of expired transient rows, the count of Action Scheduler records eligible under the current retention, and when the last run happened, with a button to run it now and a number field for the retention period.

No OPTIMIZE TABLE, no autoload audit

Deleting rows does not give disk space back or defragment indexes. That takes OPTIMIZE TABLE, which locks the whole table and can pause queries for several seconds on a big one; WP-Optimize and similar tools run it in a quiet window. This plugin does not.

Large or wrongly autoloaded options are a separate drain this plugin ignores. Some plugins store big serialised values under autoloaded keys, adding weight to every request whether the value is used or not, and finding them needs a tool that lists options by size and autoload status.

Revisions, trashed posts, spam comments, orphaned postmeta and session rows from other plugins are out of scope too. If any of those are part of the bloat, a general cleanup tool covers them.

When WP-Optimize is the better choice

If you also need to clear revisions, trash, spam and orphaned postmeta, WP-Optimize does all of it in one sweep and has a button for OPTIMIZE TABLE afterwards. To audit which options are autoloaded and how big they are, Advanced Database Cleaner breaks them down row by row.

This plugin suits the store where expired transients and a swollen Action Scheduler table are the specific problem: it handles those two, runs daily with nothing to configure beyond the retention period, and adds nothing else. A follow-up message can add a log of each run, showing the last ten on the admin page, and a weekly summary email of what was deleted.

What you get

Included

  • Daily scheduled deletion of expired transients from wp_options, removing both the timeout and value rows for each expired key
  • Deletion of completed, failed and canceled Action Scheduler records older than a configurable number of days
  • Deletion of associated Action Scheduler log rows in the same pass to avoid orphaned records
  • Admin page under WooCommerce > DB Cleanup with current row counts, a manual run button and a retention day setting

Not included

  • OPTIMIZE TABLE after row deletion: reclaiming disk space and defragmenting indexes requires a table lock this plugin does not run
  • Autoloaded options audit: large or incorrectly autoloaded options slow every page load and need a separate tool to identify
  • Post revisions, trashed posts, spam comments and orphaned postmeta rows
  • Transients stored in an external object cache: if Redis or Memcached is active, transients bypass wp_options and there are no rows to delete
  • Session rows, user metadata or tables created by third-party plugins

WP-Optimize does the things on the right. This build covers the part most sites use.

Questions

Does this touch transients if Redis or Memcached is installed?

No. With an object cache active, WordPress keeps transients in the cache instead of wp_options, so there are no rows to delete. Expect the expired transient count on the admin page to be zero or close to it.

What retention period should I set for Action Scheduler records?

Seven days covers most debugging. If you run subscriptions or often investigate failed jobs, fourteen gives more history. A typical store has no reason to keep records beyond thirty days, and keeping them longer keeps the table large.

Why does the Action Scheduler table still have thousands of rows after cleanup?

Only complete, failed and canceled records are removed; pending and in-progress ones stay. If most of what remains is pending, a plugin is scheduling jobs faster than they run, or the WP-Cron runner is not being called reliably.

Will the first run lock the database or slow the site?

Transients are deleted row by row; Action Scheduler records with one query per run. With hundreds of thousands of eligible rows, the first manual run can take a few seconds and may briefly lock tables on shared hosting. Run it once by hand to clear the backlog, then let the daily schedule handle smaller amounts.

Build it in Steem

The button opens the builder with this guide's prompt filled in, so you can change anything before it runs. Nothing is built, and nothing is charged, until you send it.

Build this pluginAbout 55 credits. The free plan includes enough for one.
Read the prompt (331 words)

Build a WordPress plugin called WC DB Cleanup. Plugin Name: WC DB Cleanup. Version: 1.0.0. Description: Daily cleanup of expired transients and completed Action Scheduler records, with a manual run page under WooCommerce > DB Cleanup. Wrap all code in a class WC_DB_Cleanup with a public init() method. Instantiate the class and call init() on plugins_loaded. Stored option: wc_db_cleanup_as_days (integer, default 7). Number of days to retain completed, failed and canceled Action Scheduler records before deleting them. Cleanup method run_cleanup(): Step one, expired transients. global $wpdb; $keys = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE _transient_timeout_% AND option_value + 0 < UNIX_TIMESTAMP()"); foreach ($keys as $k) { $wpdb->delete($wpdb->options, ['option_name' => $k]); $wpdb->delete($wpdb->options, ['option_name' => str_replace('_transient_timeout_', '_transient_', $k)]); } Step two, Action Scheduler. $days = max(1, (int) get_option('wc_db_cleanup_as_days', 7)); $cutoff = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $at = $wpdb->prefix . 'actionscheduler_actions'; $lt = $wpdb->prefix . 'actionscheduler_logs'; if ($wpdb->get_var("SHOW TABLES LIKE '{$at}'")=== $at) { $ids = $wpdb->get_col($wpdb->prepare("SELECT action_id FROM {$at} WHERE status IN ('complete','failed','canceled') AND scheduled_date_gmt < %s", $cutoff)); if ($ids) { $ids = array_map('intval', $ids); $ph = implode(',', $ids); $wpdb->query("DELETE FROM {$lt} WHERE action_id IN ($ph)"); $wpdb->query("DELETE FROM {$at} WHERE action_id IN ($ph)"); } } update_option('wc_db_cleanup_last_run', current_time('mysql')); Cron: In init(), add_action('wc_db_cleanup_daily', [$this, 'run_cleanup']); if (!wp_next_scheduled('wc_db_cleanup_daily')) { wp_schedule_event(time(), 'daily', 'wc_db_cleanup_daily'); } register_deactivation_hook(__FILE__, function() { wp_clear_scheduled_hook('wc_db_cleanup_daily'); }); Admin page: In init(), add_action('admin_menu', [$this, 'add_admin_page']). In add_admin_page(): add_submenu_page('woocommerce', 'DB Cleanup', 'DB Cleanup', 'manage_woocommerce', 'wc-db-cleanup', [$this, 'render_admin_page']). In render_admin_page(): handle two POST submissions. POST action wc_db_cleanup_run with nonce wc_db_cleanup_action: call run_cleanup() and show a success admin notice. POST action wc_db_cleanup_save with nonce wc_db_cleanup_settings: update_option('wc_db_cleanup_as_days', max(1, intval($_POST['as_days'] ?? 7))). Then display: count of expired transient rows, count of eligible Action Scheduler records if the table exists using the current saved days, last run time from option wc_db_cleanup_last_run. Then a form with a submit button 'Run cleanup now' (hidden action=wc_db_cleanup_run, nonce wc_db_cleanup_action). Then a form with a number input name=as_days min=1 and a submit button 'Save' (hidden action=wc_db_cleanup_save, nonce wc_db_cleanup_settings). Use esc_html and esc_attr throughout. No stylesheet, no script.