WooCommerce Database Bloat: Finding and Fixing It

Almost all WooCommerce database bloat sits in four tables. Here is how to measure which one is actually costing you, clear it safely, and stop it coming back.

By.

•

min read

A tall stack of pale slabs bulging outward in the middle, beside one small amber slab on its own plinth

Database tables growing unevenly, next to a single clean table

If your WooCommerce store has slowed down over the years and nobody changed the theme, the database is the first place to look — and in almost every case the bloat is concentrated in four places: autoloaded rows in wp_options, the Action Scheduler tables, wp_postmeta, and woocommerce_sessions. Not the product catalogue, not the media library.

The second thing worth saying upfront: deleting rows is the easy part and the least durable part. A store that accumulated 400,000 expired transients will accumulate them again in three months unless you fix whatever is writing them. Cleanup buys you a measurable improvement today. Fixing the writer is what keeps it.

Below is how to find which table is actually hurting you, what each one means, and the order to fix things in without taking the store down.

What database bloat actually means in WooCommerce

“Bloat” is a vague word and it gets used for two different problems that have different fixes.

Problem one: rows that get read on every single request. This is the expensive kind. Autoloaded options in wp_options are loaded into memory on every page load, logged-in or not, cached or not. A 6 MB autoload payload is 6 MB of query, transfer and PHP unserialisation before WordPress has decided what page you asked for. This directly inflates TTFB.

Problem two: tables that are simply large. A 2 GB wp_postmeta table is not inherently slow — MySQL is good at large tables with correct indexes. It becomes slow when a query has to scan it, which in WooCommerce usually means a meta query on an unindexed meta_key, or an admin screen filtering orders. It also makes backups slower, migrations riskier, and ALTER TABLE operations genuinely dangerous.

Most “slow store” complaints are problem one. Most “the backup times out” complaints are problem two. Work out which one you have before you start deleting anything.

How to find which table is the problem

Start with table sizes. Run this against your database (phpMyAdmin, Adminer, or wp db query):

SELECT table_name,
       ROUND(((data_length + index_length) / 1024 / 1024), 1) AS size_mb,
       table_rows
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC
LIMIT 15;

Then check autoload separately, because a small table can still be the problem:

SELECT ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) AS autoload_mb,
       COUNT(*) AS rows_autoloaded
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on');

SELECT option_name,
       ROUND(LENGTH(option_value) / 1024, 1) AS size_kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on')
ORDER BY LENGTH(option_value) DESC
LIMIT 20;

Note the autoload IN (...) list. WordPress 6.6 replaced the old two-value column with a wider set of values, and 6.7 formally deprecated 'yes' and 'no' in add_option() and wp_set_option_autoload(). Older cleanup queries that only check autoload = 'yes' will under-report on a current site. If your table prefix is not wp_, substitute it.

Here is what the numbers mean in practice.

What you measuredTypically fineWorth investigatingActively hurting you
Autoloaded option dataUnder 800 KB800 KB – 2 MBOver 2 MB
Rows in actionscheduler_actionsUnder 20,00020,000 – 150,000Over 150,000, or growing weekly
wp_postmeta rows per productUnder ~4040 – 100Over 100
Rows in woocommerce_sessionsRoughly 1–2 days of trafficA week of trafficMonths of traffic
Expired _transient_timeout_ rowsA few hundredThousandsTens of thousands

These are working thresholds, not standards — a store doing 500 orders a day will sit higher on several of these rows and be perfectly healthy. The signal you care about is the trend, not the absolute number. A table that doubles every month has a writer problem regardless of its current size.

The four tables that cause almost all of it

Autoloaded options in wp_options

This is the one most likely to be costing you response time today. Typical offenders: plugins caching API responses in options instead of transients, licence and update-check payloads, abandoned plugins that left orphaned settings behind, and option values holding serialised arrays that grew without bound.

WordPress 6.6 added a guard here. Options above a size threshold are no longer autoloaded by default, and that threshold is filterable:

// Default is 150000 bytes (~146 KB).
apply_filters( 'wp_max_autoloaded_option_size', 150000, $option );

That helps with new, very large options. It does nothing about the far more common pattern: 3,000 rows at 2 KB each, all autoloaded, none individually large enough to trip the threshold. For those you identify them from the query above and switch them off individually:

wp option get some_bloated_option --format=json   # inspect first
wp eval "wp_set_option_autoload( 'some_bloated_option', false );"

Do not bulk-flip autoload with a single UPDATE statement. Some options genuinely need to be autoloaded and the site will start issuing an extra query per request for each one you got wrong, which is the opposite of the outcome you wanted.

Action Scheduler

Action Scheduler is the background job queue WooCommerce and dozens of other plugins run on. It creates actionscheduler_actions, actionscheduler_claims, actionscheduler_groups and actionscheduler_logs, and on a busy store these grow fast.

It does clean up after itself. By default completed and cancelled actions are deleted after 2,678,400 seconds — 31 days — and failed actions are kept three times as long, 8,035,200 seconds. Both are filterable:

// Keep completed/cancelled actions for 7 days instead of 31.
add_filter( 'action_scheduler_retention_period', function () {
    return 7 * DAY_IN_SECONDS;
} );

Two things break this. First, if the cleanup job itself can’t run — a dead WP-Cron, a queue that is permanently backed up — nothing gets deleted and the table only grows. Second, a plugin scheduling thousands of actions per hour will outrun a 31-day retention window regardless. If actionscheduler_actions is your largest table, group by hook and find out who is responsible before you touch retention:

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

That query usually names the culprit in one line. Shortening retention on a queue that is generating 50,000 actions a day treats the symptom and leaves the cause.

wp_postmeta

On legacy stores this is where order data lives, and it is the table that makes old WooCommerce installs feel heavy. Every order historically wrote dozens of rows here. Products add their own: variations, stock, attributes, plus whatever your plugins attach. The table has an index on meta_key but not on meta_value, so any query filtering by value scans — which is why an admin order search on a large legacy store can take ten seconds or more.

Orphaned rows accumulate from deleted posts that didn’t clean up their meta:

SELECT COUNT(*)
FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;

If that returns a large number, those rows are genuinely dead and safe to remove — but delete them in batches of a few thousand, not in one statement, or you will lock the table during the delete. The same caution applies to the catalogue side of this problem, which we covered in more depth in why WooCommerce slows down after 10,000 products.

woocommerce_sessions and transients

Every visitor who touches a cart gets a session row. These expire, and WooCommerce has a scheduled job to clear expired ones — which, again, depends on cron actually running. A store with a broken cron and a year of traffic can carry hundreds of thousands of dead session rows, and because the cart reads this table on most front-end requests, it is not a harmless pile.

Expired transients stored in wp_options are the same story: WordPress deletes them lazily, on read, so a transient nothing ever reads again sits there indefinitely. Check cron health first — wp cron event list — because if cron is dead you have several of these problems at once and one fix resolves all of them.

If this sounds like your store and you would rather have someone measure it than guess, send us the site and we will tell you which table is the problem before anyone deletes a row.

What HPOS changes, and what it doesn’t

High-Performance Order Storage moves orders out of wp_posts and wp_postmeta into four purpose-built tables: wc_orders, wc_order_addresses, wc_order_operational_data and wc_orders_meta. Order queries stop fighting the shared post tables, and admin order screens get dramatically faster on stores with large order histories.

Three things people get wrong about it:

  • It is not automatic for existing stores. HPOS has been the default for new installations since WooCommerce 8.2 (October 2023), but existing stores are opt-in and stay on legacy storage until someone changes the setting under WooCommerce → Settings → Advanced → Features.
  • Migrating does not shrink your database. It copies orders into the new tables. The legacy rows in wp_posts and wp_postmeta stay where they are — which is what makes rollback instant, and also why stores report their database getting bigger after migration.
  • Compatibility mode doubles the writes. Leaving sync on permanently means every order write happens twice, to both storage systems. It is the right setting for a transition period while you confirm your plugins behave. It is the wrong setting to still have on a year later.

One related change worth knowing if you are on a recent version: WooCommerce 10.7 disabled HPOS “sync on read” by default. That was the mechanism that let data written directly to the posts table flow back into HPOS during reads, and it could cause stale data to overwrite current order records. It is controlled by the woocommerce_hpos_enable_sync_on_read filter, and the current stable release at the time of writing is 11.2.2 (4 November 2026).

The migration itself runs in the background in batches, or via WP-CLI:

wp wc cot sync

A cleanup order that won’t break the store

Database work is one of the few maintenance tasks that can destroy a store rather than merely slow it. The sequence matters more than the individual steps.

  1. Take a backup and verify it restores. Not “a backup exists” — an actual test restore. We wrote about why that distinction matters in testing your WordPress recovery.
  2. Measure first. Record table sizes, autoload total, and a few real page timings. Without a before, you cannot tell whether the cleanup helped or whether you got lucky with cache warming.
  3. Fix cron. If scheduled events aren’t running, half the bloat will clear itself once they do, and you’ll avoid deleting things the platform would have handled.
  4. Clear the genuinely dead data — expired transients, expired sessions, orphaned postmeta, old Action Scheduler rows. In batches.
  5. Fix autoload on the specific large options you identified, one at a time.
  6. Find and stop the writer. Whichever table was largest, something filled it. If you skip this step you are booking the same job again next quarter.
  7. Only then consider structural changes — HPOS migration, added indexes, archiving old orders.

Run steps 4 onwards on a staging copy first if the store takes real money. And schedule it: database maintenance belongs on a recurring list alongside everything else in a standard maintenance routine, not in an emergency after the site slows down.

When the database isn’t actually your problem

It’s worth being honest about the ceiling here. Database cleanup reliably improves TTFB and admin responsiveness. It does very little for perceived front-end speed if your real problem is render-blocking assets, oversized images, or a page builder generating 400 KB of CSS.

A useful tell: if your pages are slow for logged-out visitors on a cached page, the database is probably not the bottleneck, because a properly cached page doesn’t touch it. If the slowness shows up on cart, checkout, My Account and wp-admin — the uncacheable pages — the database is a strong suspect. Checkout in particular has its own set of technical failure modes, which we went through in the technical causes of checkout abandonment.

The short version

Measure the four tables. Find which one is large or growing. Clear the dead rows in batches, fix autoload individually, and then work out what wrote all that data in the first place — because that last step is the only one that keeps the result. HPOS is worth doing on a store with a long order history, but migrate it as a deliberate project with a tested rollback, not as a cleanup step.

If you would rather not run DELETE statements against a live store yourself, that’s a reasonable instinct. Tell us what your store does and how old it is, and we’ll audit the database, tell you exactly which table is costing you and what it would take to fix — including whether an HPOS migration is worth the disruption for your order volume.