How I Reclaimed 14GB of Database Space from a Bloated Action Scheduler

· 10 min read

A client messaged me on a Friday afternoon — their WooCommerce store's nightly backup had failed. The hosting provider's automated backup system had timed out because the database had grown to 18.4GB. This was a mid-sized store doing around 150 orders a day. There was no reason for a database that large.

Finding the Culprit

I SSH'd into the server and checked which tables were eating all the space:

SELECT table_name,
       ROUND(data_length / 1024 / 1024, 2) AS data_mb,
       ROUND(index_length / 1024 / 1024, 2) AS index_mb,
       table_rows
FROM information_schema.tables
WHERE table_schema = 'wp_production'
ORDER BY data_length DESC
LIMIT 10;

The results told the whole story:

Table Data (MB) Rows
wp_actionscheduler_actions 8,412 26,700,000
wp_actionscheduler_logs 5,891 41,200,000
wp_posts 1,240 385,000
wp_postmeta 980 2,100,000

Two tables — wp_actionscheduler_actions and wp_actionscheduler_logs — accounted for 14.3GB of the 18.4GB database. The wp_posts table, which held the actual content, orders, and products, was barely over a gigabyte.

Understanding the Problem

Action Scheduler is the background job system built into WooCommerce. Every order confirmation email, every subscription renewal check, every webhook delivery, every inventory sync — they all run through Action Scheduler. Each execution creates a row in wp_actionscheduler_actions and one or more rows in wp_actionscheduler_logs.

By default, Action Scheduler cleans up completed and canceled actions older than 31 days. It runs this cleanup every minute, deleting 20 rows per batch. The maths doesn't work on a busy store. If a site generates 5,000 actions per day (not unusual for a WooCommerce store with subscriptions, emails, and analytics plugins), that's 155,000 actions per month. At 20 deletions per cleanup cycle, it would take the built-in cleanup over 5 days of continuous work just to clear one month's worth — and new actions are being created the entire time.

There's a second problem: failed actions are never cleaned up. The default cleanup only targets complete and canceled statuses. Failed actions sit in the table forever. On this site, a broken webhook endpoint had been silently generating failed actions for months — over 3 million of them.

The Cleanup

I couldn't just truncate the tables. Pending and in-progress actions are live — they include scheduled subscription renewals, queued emails, and other critical tasks. Deleting those would break the store.

Step 1: Assess What's Safe to Remove

First, I checked the breakdown by status:

SELECT status, COUNT(*) AS total
FROM wp_actionscheduler_actions
GROUP BY status;
+------------+----------+
| status     | total    |
+------------+----------+
| complete   | 19847231 |
| failed     | 3142876  |
| canceled   | 3698441  |
| pending    | 11204    |
| in-progress|      248 |
+------------+----------+

Over 26.6 million rows were safe to delete. Only 11,452 were active.

Step 2: Batch Delete with WP-CLI

I could have run raw SQL, but WP-CLI's action-scheduler clean command properly handles the log table cleanup and respects any hooks other plugins might have registered. The key is increasing the batch size and including failed actions:

wp action-scheduler clean \
  --status=complete,failed,canceled \
  --before='7 days ago' \
  --batch-size=1000 \
  --batches=0

The --batches=0 flag tells it to keep going until everything matching the criteria is deleted. On a table this large, this took about 45 minutes. I ran it inside screen so an SSH disconnect wouldn't kill the process:

screen -S as-cleanup
wp action-scheduler clean \
  --status=complete,failed,canceled \
  --before='7 days ago' \
  --batch-size=1000 \
  --batches=0

For truly massive tables (50 million+ rows), WP-CLI can be too slow. In those cases, I use batched SQL deletes with a pause between batches to avoid locking the database:

#!/bin/bash
# Batched cleanup for extremely large Action Scheduler tables
# Only run this after taking a database backup

BATCH=50000
TOTAL=0

while true; do
  DELETED=$(mysql -u root -p"$DB_PASS" "$DB_NAME" -sN -e "
    DELETE FROM wp_actionscheduler_actions
    WHERE status IN ('complete','failed','canceled')
    AND scheduled_date_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 7 DAY)
    LIMIT $BATCH;
    SELECT ROW_COUNT();
  ")
  TOTAL=$((TOTAL + DELETED))
  echo "Deleted $DELETED rows (total: $TOTAL)"
  if [ "$DELETED" -lt "$BATCH" ]; then
    break
  fi
  sleep 2
done

echo "Cleaning orphaned logs..."
mysql -u root -p"$DB_PASS" "$DB_NAME" -e "
  DELETE FROM wp_actionscheduler_logs
  WHERE action_id NOT IN (
    SELECT action_id FROM wp_actionscheduler_actions
  );
"

Step 3: Reclaim Disk Space

Deleting rows from InnoDB tables doesn't automatically free disk space. The .ibd file stays the same size until you optimise the table:

OPTIMIZE TABLE wp_actionscheduler_actions;
OPTIMIZE TABLE wp_actionscheduler_logs;

This rewrites both tables and reclaims the freed space. On this site it took about 8 minutes per table, during which the tables are locked — so I ran it during a low-traffic window.

After cleanup and optimisation, the database dropped from 18.4GB to 4.1GB. Backups started completing in under 2 minutes again.

Preventing It from Happening Again

Cleaning up once is pointless if the tables just bloat again in a month. I added three filters to the site's mu-plugins directory:

<?php
/**
 * Plugin Name: Action Scheduler Cleanup Tuning
 * Description: Prevents Action Scheduler table bloat on high-volume WooCommerce stores.
 */

// Reduce retention from 31 days to 3 days.
// Completed actions older than 3 days have no diagnostic value
// on a store where we have proper error logging.
add_filter( 'action_scheduler_retention_period', function () {
    return 3 * DAY_IN_SECONDS;
} );

// Increase cleanup batch size from 20 to 500.
// The default of 20 cannot keep pace with a store generating
// thousands of actions per day.
add_filter( 'action_scheduler_cleanup_batch_size', function () {
    return 500;
} );

// Include failed actions in automatic cleanup.
// By default, only 'complete' and 'canceled' are purged.
// Failed actions accumulate forever unless you add them here.
add_filter( 'action_scheduler_default_cleaner_statuses', function ( $statuses ) {
    $statuses[] = 'failed';
    return $statuses;
} );

I put this in wp-content/mu-plugins/ rather than functions.php because must-use plugins load before everything else and can't be accidentally deactivated.

Fixing the Root Cause

The bloat wasn't just a cleanup problem — something was generating an abnormal number of failed actions. I checked which hooks were responsible:

SELECT hook, status, COUNT(*) AS total
FROM wp_actionscheduler_actions
WHERE status = 'failed'
GROUP BY hook, status
ORDER BY total DESC
LIMIT 10;

The top offender was woocommerce_deliver_webhook_async with over 3 million failed entries. A webhook URL configured in WooCommerce settings was pointing to a staging URL that had been decommissioned months ago. Every order, every product update, every customer registration triggered a webhook delivery that immediately failed — and each failure was dutifully logged in the Action Scheduler tables.

I removed the dead webhook, and the daily action creation rate dropped by roughly 40%.

Monitoring Going Forward

I added a simple monitoring check that alerts me if the Action Scheduler tables grow past a threshold. This runs as a server cron job once a day:

#!/bin/bash
THRESHOLD_MB=500
DB_NAME="wp_production"

SIZE=$(mysql -sN -e "
  SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024)
  FROM information_schema.tables
  WHERE table_schema = '$DB_NAME'
  AND table_name LIKE '%actionscheduler%';
")

if [ "$SIZE" -gt "$THRESHOLD_MB" ]; then
  echo "Action Scheduler tables at ${SIZE}MB (threshold: ${THRESHOLD_MB}MB)" \
    | mail -s "WP DB Alert: Action Scheduler bloat on $(hostname)" [email protected]
fi

The Takeaway

If you're running a WooCommerce store with subscriptions, webhooks, or any integration that uses background jobs, check your Action Scheduler table sizes. The defaults — 31-day retention, 20-row batch cleanup, and no failed action purging — are designed for small, low-traffic sites. On anything doing more than a few dozen orders a day, those defaults will eventually cause problems.

The signs are usually indirect: backups that take too long or fail entirely, slow admin pages (especially WooCommerce > Status > Scheduled Actions), and gradually increasing disk usage that doesn't correlate with content growth.


Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans | Get in Touch

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs