Action Scheduler Table Bloat: The Hidden WordPress Performance Killer

WooCommerce's Action Scheduler silently accumulates hundreds of thousands of completed tasks. Here's how to find the bloat, clean it safely, and keep it from coming back.

Barry van Biljon
March 17, 2026
11 min read
Action Scheduler Table Bloat: The Hidden WordPress Performance Killer
Back to Blog

Key Takeaways

  • WooCommerce's Action Scheduler stores every background task it has ever run, and never cleans up after itself

  • A store processing 50 orders per day can accumulate 200,000+ completed action rows in under a year

  • The bloated table slows down order processing, webhook delivery, and every WooCommerce background task

  • You can safely delete completed actions older than 30 days without affecting store operations

  • Setting up automated cleanup prevents the problem from coming back

The table you've never checked

If you're running WooCommerce, there's a database table quietly growing in the background every hour of every day. It's called wp_actionscheduler_actions, and there's a good chance it's one of the largest tables in your entire database.

Action Scheduler is WooCommerce's background task system. Every time your store processes an order, sends an email notification, fires a webhook, syncs product data, or runs an analytics calculation, it goes through Action Scheduler. Each of these tasks gets a row in the database.

The task runs. The row stays.

WooCommerce never fully cleans this table out. Completed tasks accumulate indefinitely. On a store that's been live for 18 months processing a moderate volume of orders, I regularly find 200,000 to 500,000 rows in this table. The worst case I've dealt with was 1.2 million rows on a store that had been running for 3 years with multiple payment gateways and a handful of marketing integrations.

This isn't a theoretical problem. When this table gets large, it slows down WooCommerce's ability to process new background tasks. Orders take longer to process. Webhook deliveries get delayed. Email notifications arrive late. And because it all happens in the background, most store owners have no idea it's happening.


How Action Scheduler works

Action Scheduler is a job queue built into WooCommerce. When your store needs to do something that doesn't have to happen immediately (sending a follow-up email after an order, notifying a fulfillment API) it creates a "scheduled action" with a target time.

A background runner picks up pending actions and executes them. Each action goes through these statuses:

StatusMeaning
pendingWaiting to run
runningCurrently executing
completeFinished successfully
failedRan but hit an error
canceledManually or programmatically canceled

The pending and running actions are the ones doing actual work. Everything else is history.

The problem is that WooCommerce treats this history like a permanent record. Completed actions stay in the table. Failed actions stay in the table. Canceled actions stay in the table. And each action has an associated log entry in a separate wp_actionscheduler_logs table that also never gets cleaned up.

What generates actions

You might think your store only creates a few actions per order. The reality is more like this:

  • Order processing: payment confirmation, status updates, stock adjustments (3-5 actions per order)
  • Email notifications: customer receipt, admin notification, shipping updates (2-4 actions per order)
  • Webhooks: if you use any integrations that listen for order events (1-3 per webhook per order)
  • Analytics: WooCommerce Analytics recalculates revenue, product, and customer data (2-5 actions per order)
  • Subscriptions: if you use WooCommerce Subscriptions, renewal checks and payment retries generate their own actions
  • Marketing plugins: Mailchimp, Klaviyo, and similar tools sync customer data through Action Scheduler

A single order can easily generate 10-15 scheduled actions. Fifty orders a day means 500-750 new rows daily. Over a year, that's 180,000-270,000 rows from orders alone, before counting all the other background tasks your plugins schedule.


How to check for bloat

Step 1: count the rows by status

Connect to your database and run:

SELECT
  status,
  COUNT(*) as count,
  MIN(scheduled_date_gmt) as oldest,
  MAX(scheduled_date_gmt) as newest
FROM wp_actionscheduler_actions
GROUP BY status
ORDER BY count DESC;

Here's what I saw on a client's WooCommerce store last month (running for about 2 years, ~40 orders/day):

StatusCountOldestNewest
complete347,2912024-03-152026-03-28
failed4,8322024-06-012026-03-27
pending1272026-03-282026-03-29
canceled2,1042024-05-202026-02-14

347,000 completed actions. 127 pending. The ratio tells the whole story. 99.96% of the table is history.

Step 2: check the table size on disk

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 = DATABASE()
  AND table_name LIKE '%actionscheduler%'
ORDER BY data_length DESC;

On that same client store:

TableData (MB)Index (MB)Rows
wp_actionscheduler_actions142.387.6354,354
wp_actionscheduler_logs98.742.1892,441

230MB of action data. 140MB of logs. Over 370MB of database space used by background task history that serves no operational purpose.

Step 3: see what's generating the most actions

SELECT
  hook,
  status,
  COUNT(*) as count
FROM wp_actionscheduler_actions
GROUP BY hook, status
HAVING count > 100
ORDER BY count DESC
LIMIT 20;

This tells you which specific tasks are filling the table. Common high-volume hooks:

  • woocommerce_run_product_attribute_lookup_update_callback: product data sync
  • action_scheduler/migration_hook: one-time migration tasks that completed long ago
  • wc-admin_import_orders: WooCommerce Analytics order imports
  • mailchimp_sync_*: Mailchimp plugin sync jobs
  • woocommerce_deliver_webhook_async: webhook deliveries

If you see a single hook with 100,000+ completed rows, that's your biggest contributor.


The performance impact

A bloated Action Scheduler table doesn't just waste disk space. It actively slows down your store in ways you might not connect to the database.

Slow background task processing

When Action Scheduler looks for pending tasks, it queries the wp_actionscheduler_actions table. On a table with 300,000+ rows, even indexed queries take longer than they should. The more rows, the slower the lookup, the more delay before your pending actions start running.

This means:

  • Order confirmation emails arrive 30-60 seconds late instead of instantly
  • Webhook notifications to fulfillment services get delayed
  • Analytics data takes longer to update
  • Subscription renewal checks fall behind

Admin panel slowdowns

The WooCommerce > Status > Scheduled Actions page loads data from these tables. With hundreds of thousands of rows, this page can take 10-15 seconds to load or time out entirely. The admin panel search and filter functions also slow down.

Database backup bloat

Every backup of your database includes the full Action Scheduler tables. If they're 370MB of historical data you don't need, every backup is 370MB larger than necessary. That's slower backup times, more storage costs, and longer restore times if you ever need to recover from an incident.

Cascading effects on wp_options

Some Action Scheduler operations write temporary data to wp_options. If the scheduler is backed up because the table is bloated, these temporary entries can pile up too. It's not uncommon to find Action Scheduler-related transients contributing to wp_options bloat on the same store.


How to clean it up

Back up your database first. Always.

mysqldump -u your_user -p your_database \
  wp_actionscheduler_actions \
  wp_actionscheduler_logs \
  wp_actionscheduler_groups \
  wp_actionscheduler_claims \
  > actionscheduler_backup.sql

Delete completed actions older than 30 days

30 days gives you enough history to troubleshoot recent order issues. Anything older than that is dead weight.

DELETE FROM wp_actionscheduler_actions
WHERE status = 'complete'
  AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 30 DAY);

On a table with 300,000+ completed rows, this can take a while. If it times out, batch it:

DELETE FROM wp_actionscheduler_actions
WHERE status = 'complete'
  AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 50000;

Run it repeatedly until it returns 0 affected rows.

Delete old failed actions

Failed actions are worth investigating if they're recent. Old failed actions are just noise.

-- Check what's failing before you delete
SELECT
  hook,
  COUNT(*) as fail_count,
  MAX(scheduled_date_gmt) as last_failure
FROM wp_actionscheduler_actions
WHERE status = 'failed'
GROUP BY hook
ORDER BY fail_count DESC;

If you see recurring failures for a specific hook, that's a problem to fix (dead webhook endpoint, misconfigured email, broken plugin). Once you've noted what needs attention:

DELETE FROM wp_actionscheduler_actions
WHERE status = 'failed'
  AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 7 DAY);

Delete canceled actions

Canceled actions are always safe to remove. They were explicitly stopped before running.

DELETE FROM wp_actionscheduler_actions
WHERE status = 'canceled';

Clean up orphaned logs

The wp_actionscheduler_logs table stores detailed execution logs for each action. After deleting actions, you'll have log entries that reference actions that no longer exist.

DELETE FROM wp_actionscheduler_logs
WHERE action_id NOT IN (
  SELECT action_id FROM wp_actionscheduler_actions
);

On large tables, this subquery can be slow. The JOIN version is faster:

DELETE l FROM wp_actionscheduler_logs l
LEFT JOIN wp_actionscheduler_actions a ON l.action_id = a.action_id
WHERE a.action_id IS NULL;

Batch it if needed:

DELETE l FROM wp_actionscheduler_logs l
LEFT JOIN wp_actionscheduler_actions a ON l.action_id = a.action_id
WHERE a.action_id IS NULL
LIMIT 50000;

Optimize the tables

After deleting hundreds of thousands of rows, reclaim the disk space:

OPTIMIZE TABLE wp_actionscheduler_actions;
OPTIMIZE TABLE wp_actionscheduler_logs;

Verify the results

Run the status count query from Step 1 again. On the client store I mentioned earlier:

MetricBeforeAfter
Total action rows354,35412,847
Total log rows892,44131,206
Actions table size142.3 MB4.8 MB
Logs table size98.7 MB6.2 MB

From 370MB down to 11MB. Background tasks started processing noticeably faster. The admin Scheduled Actions page loaded in under 2 seconds. Database backups dropped by over 350MB.


Keeping it clean

Cleaning once is good. Preventing the problem from coming back is better.

Option 1: WooCommerce's built-in retention

WooCommerce has a setting for action retention, but it's not exposed in the admin panel. You can set it with a filter in your theme's functions.php or a custom plugin:

add_filter('action_scheduler_retention_period', function() {
    return 30 * DAY_IN_SECONDS; // Keep 30 days of history
});

This tells Action Scheduler to clean up completed actions older than 30 days. The default is supposed to be 30 days, but in practice the cleanup job often falls behind on busy stores because it processes in small batches and competes with actual business tasks for execution slots.

Option 2: scheduled SQL cleanup via cron

If the built-in cleanup can't keep up, add a system cron job that runs the SQL directly:

# Run weekly on Sunday at 3am server time
0 3 * * 0 mysql -u your_user -pyour_password your_database -e "
  DELETE FROM wp_actionscheduler_actions
  WHERE status IN ('complete','failed','canceled')
  AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 30 DAY);
 
  DELETE l FROM wp_actionscheduler_logs l
  LEFT JOIN wp_actionscheduler_actions a ON l.action_id = a.action_id
  WHERE a.action_id IS NULL;
 
  OPTIMIZE TABLE wp_actionscheduler_actions;
  OPTIMIZE TABLE wp_actionscheduler_logs;
" > /dev/null 2>&1

This is what I use on the WooCommerce stores I manage. It runs at a quiet time, cleans everything over 30 days, removes orphaned logs, and optimizes the tables. The store owner never has to think about it.

Option 3: WP-CLI script

If you have WP-CLI available, you can use the built-in Action Scheduler CLI commands:

wp action-scheduler clean --status=complete --before="30 days ago"
wp action-scheduler clean --status=failed --before="7 days ago"
wp action-scheduler clean --status=canceled

Wrap it in a cron job the same way.


What about the other Action Scheduler tables?

The full set of Action Scheduler tables is:

  • wp_actionscheduler_actions: the main table (this is where the bulk of the bloat lives)
  • wp_actionscheduler_logs: execution logs for each action
  • wp_actionscheduler_groups: action groups (usually small, rarely needs cleanup)
  • wp_actionscheduler_claims: temporary claim records for in-progress batches (should be near-empty when no actions are running)

The groups and claims tables are typically tiny and don't need attention. Focus your cleanup on actions and logs.


When to worry about failed actions

A few failed actions per week is normal. Network timeouts happen. External APIs go down temporarily. These failures are usually retried automatically and succeed on the next attempt.

Start investigating when you see:

  • The same hook failing repeatedly (100+ failures). Something is consistently broken. A dead webhook URL, an expired API key, a misconfigured plugin.
  • Failed actions increasing daily. The failure rate is growing, which means whatever's causing it isn't resolving on its own.
  • Pending actions backing up. If your pending count is growing faster than actions are completing, the scheduler can't keep up. This could be a resource issue (PHP-FPM workers maxed out) or a scheduling conflict.

Check the logs for a specific failed action:

SELECT
  a.hook,
  a.scheduled_date_gmt,
  l.message
FROM wp_actionscheduler_actions a
JOIN wp_actionscheduler_logs l ON a.action_id = l.action_id
WHERE a.status = 'failed'
ORDER BY a.scheduled_date_gmt DESC
LIMIT 20;

The log messages usually tell you exactly what went wrong.


The bigger picture

Action Scheduler bloat is one piece of a larger WooCommerce database performance problem. The wp_postmeta table storing product data, the wp_options table accumulating session data and transients, and orphaned data from deleted orders and products all contribute to progressive slowdown.

If you're dealing with a slow WooCommerce store, Action Scheduler is worth checking, but it's rarely the only issue. Start with wp_options cleanup for the highest impact, then work through the full diagnostic checklist to catch everything.


Barry van Biljon

Written by

Barry van Biljon

Connect on LinkedIn

Full-stack developer specializing in high-performance web applications with React, Next.js, and WordPress.

Ready to Get Started?

Have questions about implementing these strategies? Our team is here to help you build high-performance web applications that drive results.

Frequently Asked Questions

No. Completed actions are historical records of tasks that already ran successfully. WooCommerce doesn't reference them again. Deleting them is like clearing your browser history. The actual work was already done. Just keep the most recent 30 days in case you need to troubleshoot a recent order issue.