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.
