wp_actionscheduler_logs is huge: how to clean up Action Scheduler safely

Two tables you never created are most of your database. They belong to Action Scheduler, the job queue inside WooCommerce, and the cleanup that should empty them can't keep up. How to measure them, delete the finished rows in batches without touching pending work, and make the sweep run for real.

Founder of RoundBorders · WordPress since 2012

Published 9 min read

This post runs queries against your database. Back up before the first DELETE, and expect your tables to differ from the ones described. If you'd prefer someone else did it, the end of the post says what we charge.

The short answer

Delete the finished rows (complete and canceled) in batches and never the pending ones, then sweep the orphaned log rows, fix whatever is flooding the queue, and give the cleanup a real schedule so it keeps up.

  1. 01Measure the two tables
  2. 02Check what’s pending before you delete anything
  3. 03Delete the finished rows in batches
  4. 04Sweep the orphaned logs and get the disk back
  5. 05Find what is filling it, and fix the source
  6. 06Make the sweep actually run

Rather hand it over? Get a fixed price for the whole job, from $699.

If wp_actionscheduler_logs and wp_actionscheduler_actions are at the top of your table sizes, you’ve found the usual suspect. They belong to Action Scheduler, the background job queue that ships inside WooCommerce and a long list of other plugins, and they grow because every job it runs leaves a row behind, plus two or three log lines about it. There is a cleanup that deletes finished rows after a month. It exists, it runs, and on a busy store it loses: it sweeps twenty rows per status each time the queue runs, and the queue’s scheduled pass is once a minute at best, while the store adds hundreds. Once the backlog is in the millions, the Scheduled Actions screen stops loading, and the sweep is still twenty rows a minute.

The fix is to delete the finished rows yourself, in batches, leave the pending ones alone, and then give the cleanup a real schedule so this is the last time. Here it is in the order I did it on the 29 GB database, where these two tables were most of the 29 GB.

Step 01 Measure the two tables

One query in phpMyAdmin, Adminer, or your host’s database tool. Change wp_ if your table prefix differs; that applies to every query in this post.

SQL
SELECT table_name,
       ROUND((data_length + index_length) / 1024 / 1024) AS mb,
       table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name LIKE '%actionscheduler%'
ORDER BY (data_length + index_length) DESC;

table_rows is an estimate on InnoDB, close enough to read the shape. Then see what the rows are:

SQL
SELECT status, COUNT(*) AS actions
FROM wp_actionscheduler_actions
GROUP BY status;

What’s normal Both tables together, under a couple of hundred megabytes on a busy store and single digits on a small site. complete should be the largest status and should stop growing at about a month of history. Millions of complete rows, or a logs table several times the size of the actions table, means the sweep hasn’t kept up for a long while.

Small, and a month deep?

You're fine. Skip to step 06 so it stays that way.

Millions of rows?

Step 02 before you delete anything.

Step 02 Check what’s pending before you delete anything

The actions table is a to-do list that nobody crosses off. Each row has a status, and the status decides what’s safe:

  • pending is work still to do: subscription renewals, scheduled emails, webhook deliveries, product syncs, the analytics import. Never delete it.
  • complete and canceled are done. Safe to clear.
  • failed is the pile that tells you something is broken. Read it, then clear it.
  • in-progress is being worked on right now. Leave it.

The logs table is the diary of all of the above, one line per event, referenced by action ID. Losing history costs you nothing; losing pending work costs you orders.

Two things to look at before the deleting starts. First, whether the queue is behind:

SQL
SELECT COUNT(*) AS past_due
FROM wp_actionscheduler_actions
WHERE status = 'pending'
  AND scheduled_date_gmt < UTC_TIMESTAMP();

A handful is normal; the runner is between passes. Thousands means the queue itself has stalled, which is the other way this table makes itself known: renewals late, order emails late, a sync that never finishes. Cleaning the table helps that, but step 06 is the real fix.

Second, what has been failing:

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

The hook name usually names the plugin. A webhook whose receiver was switched off last year fails on every order, forever, and each failure is a row plus its logs.

Plain English You can also see all of this under WooCommerce → Status → Scheduled Actions (Tools → Scheduled Actions on sites without WooCommerce). On a table this size that screen often won’t load, because it counts every row by status before it can draw the tabs. That timeout is usually the first thing an owner notices.

Step 03 Delete the finished rows in batches

Take a database backup first. Then, if you have WP-CLI, use Action Scheduler’s own clean command, which deletes each finished action together with its log lines:

Terminal
wp action-scheduler clean --batch-size=500 --pause=1

By default it removes complete and canceled actions older than 31 days. The batch size is 20 unless you say otherwise, which is why the built-in sweep never caught up; 500 with a one-second pause between batches keeps the store responsive while it works. It runs until nothing is left to delete, so start it in a screen or tmux session, because on tens of millions of rows it takes hours. To include old failures once you’ve read them:

Terminal
wp action-scheduler clean --status=failed --before='90 days ago' --batch-size=500 --pause=1

No WP-CLI? The same thing in SQL, limited to ten thousand rows at a time:

SQL
DELETE FROM wp_actionscheduler_actions
WHERE status IN ('complete', 'canceled')
  AND scheduled_date_gmt < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY)
LIMIT 10000;

Run it again until it reports 0 rows affected. The LIMIT is the whole point: one giant DELETE holds locks and the undo log for the entire run, and on a shared host it gets killed at thirty seconds having done nothing. Ten thousand rows finishes in a second or two. The SQL route leaves the log lines behind; step 04 collects them.

Actions table small again?

Run step 01. If the logs table shrank with it (WP-CLI did that for you), go to step 05.

Logs still huge?

They're orphans now, rows about actions that no longer exist. Step 04.

Step 04 Sweep the orphaned logs and get the disk back

Every log line points at an action ID. Delete the ones whose action is gone, again in batches (MySQL won’t accept LIMIT on a delete that joins two tables, hence the inner query):

SQL
DELETE FROM wp_actionscheduler_logs
WHERE log_id IN (
  SELECT log_id FROM (
    SELECT l.log_id
    FROM wp_actionscheduler_logs AS l
    LEFT JOIN wp_actionscheduler_actions AS a ON a.action_id = l.action_id
    WHERE a.action_id IS NULL
    LIMIT 10000
  ) AS batch
);

Repeat until 0 rows affected. Then run step 01 again and don’t be surprised when the size hasn’t moved: InnoDB keeps deleted space inside the table file until the table is rebuilt. That’s this:

SQL
OPTIMIZE TABLE wp_actionscheduler_actions, wp_actionscheduler_logs;

It needs free disk roughly equal to the table’s current size while it copies, and it holds the table briefly at the end, so run it off-peak. On managed hosting, ask support to run it for you; most will. This is the step where the number in step 01 finally drops, and on the 29 GB site it’s where most of the 29 GB came back.

Step 05 Find what is filling it, and fix the source

Cleaning is pointless if a plugin refills the table by Friday. The hook column tells you who:

SQL
SELECT hook, status, COUNT(*) AS n
FROM wp_actionscheduler_actions
GROUP BY hook, status
ORDER BY n DESC
LIMIT 20;

Read the top of the list. Most hooks name their owner: wc-admin_import_orders is WooCommerce Analytics catching up after a big import; woocommerce_deliver_webhook_async is a webhook firing on every order, and if it’s in the failed pile, the receiver is gone; a hook with a sync plugin’s prefix (a product feed, a marketing platform, a marketplace connector) that schedules a job per product per hour will out-write everything else on the site.

The fix is at the source. A dead webhook gets deleted (WooCommerce → Settings → Advanced → Webhooks). A sync plugin usually has a frequency setting, and hourly is rarely needed. Analytics can be switched off under WooCommerce → Settings → Advanced → Features if nobody opens those screens. A plugin that schedules a job on every page load without checking whether one already exists is a bug, and a support ticket with the query above attached usually gets it fixed. Only after the source is fixed does the cleanup stick, which is exactly the lesson from the autoloaded options table: the plugin that writes is the plugin that matters.

Step 06 Make the sweep actually run

Three things, and the first is the one most sites are missing.

Put WP-Cron on a real schedule. The queue runner, and the sweep that rides on it, are triggered by WP-Cron, and WP-Cron only fires when someone visits. A page-cached store serves visitors saved HTML, so the trigger fires far less often than you’d think. The setup is in the slow wp-admin post, step 03: DISABLE_WP_CRON in wp-config.php and a server cron calling wp-cron.php every few minutes.

Shorten the retention and widen the broom. A month of finished actions is rarely useful; a week is plenty, and the default sweep of 20 rows per pass can be raised. Both are filters; put them in a small must-use plugin so they survive theme changes:

PHP
<?php
// Keep finished actions for 7 days instead of a month.
add_filter( 'action_scheduler_retention_period', function () {
	return 7 * DAY_IN_SECONDS;
} );

// Let each cleanup pass delete more than 20 rows per status.
add_filter( 'action_scheduler_cleanup_batch_size', function () {
	return 250;
} );

Since Action Scheduler 4.0, failed actions are kept for three months by their own filter, action_scheduler_retention_period_for_failed; on older versions they were never deleted at all unless you added failed to the cleaner’s statuses. The performance page in Action Scheduler’s docs has both snippets.

On a busy store, take the deleting off the queue entirely. One cron line, nightly, and the sweep no longer depends on the queue runner having spare seconds:

Terminal
0 3 * * * cd /var/www/html && wp action-scheduler clean --batch-size=500 --pause=1 --quiet

Adjust the path to your site. A real cron for the retention job is what kept the 29 GB site small afterwards. Without one, the same table comes back; it just takes a couple of years to notice.

Then run the step 01 query once a month. It’s one of the lines on the monthly health report I send to care-plan clients, because it’s the number that catches this two years before anyone notices.

Where I’d stop trying: when the batches in step 03 time out on shared hosting with no WP-CLI and no support willing to run OPTIMIZE, or when the past-due count in step 02 is in the thousands and renewals and order emails are already late. At that point the table is a symptom and the queue is the patient, and measuring that is the first hour of the performance work I do. The 29 GB story is what that work looks like when it’s finished.

From the RoundBorders team

Still slow after these steps?

Send the address and what you measured. We find where the time goes and send a fixed price, usually within a few hours.

  • From $699
  • Measured before and after
  • Nothing to pay until you approve a price
No passwords needed to quote

Write as much as you like: what's happening, what you want, links, errors. The more we know, the more exact your fixed price.

How we make WordPress fast

Share this post

Know someone whose site is slow? Send it their way.

Bishoy Awad

Written by

Founder of RoundBorders. Building and fixing WordPress sites since 2012; our work is listed in the WooCommerce, Pressable and Jetpack partner directories.

Found a mistake? Write to me.

Keep reading

More on speed