WooCommerce HPOS: What High-Performance Order Storage Changes and How to Migrate

by Francis Rozange | Oct 2, 2026 | WooCommerce

At Universal Yums, an American company that ships a box of snacks from a different country every month, the customer dashboard started slowing down at around four or five million orders. The store ran on WooCommerce, and every order was stored as a WordPress post object, alongside blog posts and pages, with its details scattered across a giant metadata table.

HPOS, for High-Performance Order Storage, is WooCommerce’s answer to that problem: order storage in dedicated tables, designed for orders. Stable since late 2023, it is on by default for new stores unless an incompatible or undeclared plugin blocks it. Older stores still have to make the move themselves.

This guide explains what HPOS changes, what version 8.2 changed and what it did not, the role of compatibility mode, how to check plugins, step-by-step migration, the useful WP-CLI commands, rolling back, code changes, and the real performance gains. It also tells how Universal Yums migrated more than ten million records.

Why WooCommerce moved orders out of wp_posts

Orders stored like posts

For more than ten years, WooCommerce stored each order as a WordPress post, in the wp_posts table. The amount, the address, the payment method and dozens of other details went into wp_postmeta, a key and value table shared with all of the site’s content.

This model had the advantage of simplicity. It showed its limits with volume: every order search by customer or email address forced the database to dig through a metadata table whose values are not indexed. On a large store, that table easily exceeds millions of rows.

The four HPOS tables

HPOS stores orders in four dedicated tables. The wc_orders table holds the essentials: status, currency, total, customer, billing email, dates, payment method. The wc_order_addresses table stores one row per address, billing and shipping.

The wc_order_operational_data table keeps operational data, such as the order key, the WooCommerce version or the payment date. Finally, wc_orders_meta holds the metadata plugins add, with indexed values, unlike wp_postmeta. Order line items stay in the woocommerce_order_items tables WooCommerce already used.

Placeholder posts and order IDs

Even with HPOS on, every order keeps a row in the posts table, a full copy while synchronization runs, then a placeholder post of type shop_order_placehold once it is off or after a cleanup. It reserves the ID, so an order’s number stays equal to its post’s. These traces remain even after the legacy data is cleaned up.

What version 8.2 changed, and what it did not

The default for new stores, with conditions

HPOS had been an experimental opt-in since WooCommerce 7.1, in November 2022. Version 8.2, released on October 10, 2023, declared it stable and enabled it by default for new installations.

The code sets two conditions, though: the store must have no orders, and no active plugin that declares itself WooCommerce-aware may be incompatible or silent on the matter. A fresh install with an undeclared plugin therefore starts on the legacy storage.

Existing stores: still a choice in 2026

The merchant documentation is clear: existing stores are not migrated automatically, and the feature remains entirely opt-in. In October 2026, we found no official date for removing posts-based storage. Be wary of articles announcing its imminent removal: WooCommerce has published nothing of the kind.

The setting lives under WooCommerce, Settings, Advanced, Features. The “Order data storage” section offers two choices: “High-performance order storage (recommended)” and “WordPress posts storage (legacy)”. Below them, a checkbox turns on compatibility mode.

Compatibility mode and synchronization

Authoritative tables and backup tables

At any time, only one of the two stores is authoritative: it is the one WooCommerce reads and writes. Compatibility mode adds synchronization to the other store, which then serves as a backup copy.

As long as synchronization runs, switching back to the legacy storage is instant. Background synchronization goes through Action Scheduler jobs, visible under WooCommerce, Status, Scheduled Actions. WooCommerce forbids switching storage while orders remain unsynced, with an unambiguous warning about the risk of data corruption.

Sync on read, off since version 10.7

A second mechanism, sync on read, copied into HPOS the changes a plugin wrote directly to the legacy tables, at the moment the order was read. It caused several documented incidents.

In November 2024, a ticket described orders mysteriously turning pending and losing their metadata, fixed in the code in October 2025 and shipped with version 10.4 in December 2025. In December 2025, another described order.updated webhooks firing in a loop: reading an order triggered a sync, hence a change, hence a new delivery. Version 10.4.3 fixed that loop a few days later.

WooCommerce drew the conclusion. Since version 10.7, released on April 14, 2026, sync on read is off by default, and the team points out that compatibility mode and this sync were only ever meant as transition measures. The copy in the legacy tables must be treated as read-only. Code that still writes directly to wp_postmeta is the real problem to fix.

Subscriptions, a case to watch

Subscription stores had their own alert. In April 2026, Sybre Waaijer, founder of The SEO Framework plugin, publicly reported four WooCommerce Subscriptions bugs that, in his account, left subscriptions on manual renewal. WooCommerce confirmed all four, but narrowed their scope: only two, combined, produced that effect. The last one was fixed in version 8.6.1 of the plugin, on April 23.

On April 30, WooCommerce published, under Darren Ethier’s name, a tool to check the health of subscriptions. According to that account, the exposure concerned HPOS stores, mostly from October 2023 to March 2024, with a tail until May 9, 2024. If you sold subscriptions on HPOS during that period, run the tool.

Checking your plugins before switching

The Features screen and the incompatible plugins list

Plugins declare their HPOS compatibility in their code. If an active plugin declares itself incompatible, WooCommerce disables the HPOS option on the Features screen and offers a link to the list of plugins concerned.

One subtlety matters a lot: only plugins carrying the “WC tested up to” header are checked. For those plugins, no declaration counts as incompatibility. A plugin without that header is not checked at all, and it can break without warning if it writes directly to the legacy tables.

The compatibility-info command

WP-CLI draws up the same inventory on the command line, since WooCommerce 9.1. The command sorts plugins into compatible, incompatible and uncertain.

wp wc hpos compatibility-info --include-inactive --display-filenames

A plugin that still has not declared its compatibility in 2026 deserves a direct question to its vendor, and often a replacement.

Custom code and external systems

Plugins are only part of the picture. Custom code in the theme or in-house plugins, hand-written SQL queries, and external systems that read the database directly, such as a data warehouse, accounting or shipping software, must be audited too. The official guide for large stores points out that these external readers escape a standard code audit.

Migrating step by step

Test locally, then on a staging copy

WooCommerce’s guide for large stores describes three phases. Locally, test checkout with every payment method, refunds, subscription purchases and renewals and your critical journeys, with synchronization on and then off.

On a staging copy fed with the production database, turn on synchronization and time it. On WooCommerce’s test store, nine million orders took about a week. If you first need to build that copy, our method to migrate a WordPress site without downtime covers the preparation.

Plugins with their own content types

The merchant documentation adds an instruction that is easy to forget: plugins that store their own order-related content types, such as WooCommerce Subscriptions or WooCommerce Bookings, must stay active throughout the transition. Deactivating them before the switch can create discrepancies between the two stores.

Background synchronization moves in batches. The documentation speaks of twenty-five orders at a time, but the code processes two hundred and fifty when the legacy tables are authoritative, and twenty-six in the other, slower direction. A filter lets you adjust that size if your scheduled jobs have headroom.

In production: sync, then switch

In production, turn on synchronization while keeping the legacy tables authoritative, then run the migration. Interrupting the sync is safe, according to the guide. Once everything is synchronized, make HPOS authoritative while keeping synchronization on: rolling back stays instant, with no downtime.

wp wc hpos sync
wp wc hpos count_unmigrated
wp wc hpos enable --with-sync

Turning compatibility mode off, and when

On its own high-volume store, WooCommerce turned off sync on read after six hours, then all synchronization after a week. The team then ran a manual sync from time to time to keep a fallback.

The guide notes that keeping synchronization on, with the legacy tables authoritative, had no noticeable negative effect on performance. That observation applies while the legacy tables are authoritative. Once HPOS is authoritative, the guide recommends turning synchronization off step by step, and WooCommerce notes that turning compatibility mode off can improve performance. One week of stable operation is the guide’s marker.

Two parallel channels of light mirroring each other, joined by thin bridges

The WP-CLI commands that matter

status, sync and count_unmigrated

Since WooCommerce 8.9, every command lives under wp wc hpos. The old wp wc cot commands still run with a warning, even though some documentation pages still quote them. The status command gives the overall state: HPOS on or off, compatibility mode, unsynced orders and orders subject to cleanup.

wp wc hpos status
wp wc hpos sync --batch-size=500

Our selection of essential WP-CLI commands covers the tool itself.

verify_data, diff and backfill for diagnosis

The verify_data command compares the legacy tables with the HPOS tables, so it only matters when compatibility mode is on. Watch out for a typo in the official large-store guide, which quotes a verify_cot_data command under wp wc hpos, which does not exist under that name.

wp wc hpos verify_data --verbose
wp wc hpos diff 1234
wp wc hpos backfill 1234 --from=hpos --to=posts

The diff command shows the differences for a specific order without fixing anything, and backfill copies an order from one store to the other. The --re-migrate option of verify_data calls for caution: the code warns that it can overwrite recent data with stale data.

HPOS tables and WP-CLI’s scope

A trap awaits anyone who moves the database with WP-CLI. By default, commands such as wp db tables or wp search-replace only handle the tables WordPress knows about. WooCommerce registers about ten of its tables there, but not the four HPOS tables.

A domain replacement can therefore miss the order data that contains it, such as URLs stored in order metadata or email addresses on the old domain. Add the --all-tables-with-prefix option to cover every table that carries the site’s prefix.

cleanup, the point of no easy return

The cleanup command deletes the legacy order data, once HPOS is authoritative and synchronization is off. It keeps the placeholder posts. After it, going back to the legacy storage means rebuilding every order from HPOS: only run it after weeks of incident-free operation.

The Universal Yums case: ten million records

Devin Price, CTO of Universal Yums, described his store’s migration on his blog, DevPress, in spring 2024. WooCommerce later featured the company twice on its own site. The story brings together everything the documentation describes, at a scale that magnifies every detail.

A subscription store at scale

At the time of the migration, the store held more than ten million orders and subscriptions. On the first day of each month, it generates more than one hundred and twenty thousand renewal orders. Before HPOS, the team had piled up workarounds: to find a customer’s orders quickly, it stored the customer ID in a wp_posts column repurposed for the job.

The plan they chose, without compatibility mode

The audit started with the code. Most plugins were already declared compatible, but the store had more than one hundred thousand lines of custom code. About five hundred automated tests alone caught nearly 80% of the features that needed work. One affiliate plugin was never updated: the team wrote its own integration.

Devin Price then set aside the standard path. Compatibility mode’s double writes worried him, especially if the background migration spilled into a renewal day. On the staging copy, the HPOS settings page would not even load once the migration started, probably because of the pending orders count over more than ten million records.

So the team wrote its own script, able to migrate about three million orders an hour. It first migrated completed orders and cancelled subscriptions, then deleted their legacy data. One night, at three in the morning, the quietest hour, the store went into maintenance to migrate the rest and switch to HPOS. The only immediate issues came from the new addresses of the order screens in the admin.

Renewal day: the deadlocks

The real test came with the first renewal cycle. About a quarter of renewals failed. The log showed deadlocks, situations where two transactions block each other, on inserts into the order addresses table. The renewal order was never created.

The team first recovered the affected subscriptions with a script, then reduced the number of batches Action Scheduler processed in parallel. At twenty concurrent batches, a quarter of renewals failed. At ten, one in ten. At five, one in twenty, at the cost of lower throughput. On top of that, a hook rescheduled any renewal that hit a deadlock one minute later. Our guide to WP-Cron and system cron explains how Action Scheduler runs these jobs.

What version 9.0 changed, and what the store gained

In late May 2024, Devin Price wrote that the very responsive WooCommerce team would ship a fix in version 9.0. On May 22, a change merged for that version replaced the address table’s insert query with a read followed by an insert or update, which holds fewer locks, citing subscription renewals.

The two sources do not mention each other, but everything matches. WooCommerce 9.0 shipped on June 18, 2024.

The outcome reads in two stages. In May 2024, on DevPress, Devin Price wrote that he had seen no noticeable performance gain, since the store was already heavily optimized, and expected more for large stores without a dedicated performance team. In 2025, interviewed by WooCommerce, he explained that HPOS let the team remove all its workarounds, that the dashboard stays just as fast, and that HPOS made things much faster.

For a smaller store, the lesson is twofold. Automated tests and an audit of custom code find most of the problems. And skipping compatibility mode means giving up instant rollback. Such a decision makes sense at that scale, and a smaller store has every reason to keep instant rollback.

Rolling back

With compatibility mode on: instant

As long as synchronization is on, going back to the legacy storage is immediate. Turn compatibility mode on if it is off, wait for synchronization to finish, select “WordPress posts storage (legacy)”, then save.

Sync off or cleanup done: rebuild first

If synchronization is off, rolling back is still possible, but you have to wait for background jobs to rebuild the legacy tables. After a cleanup, every order has to be copied back from HPOS. In every case, a full backup taken before the switch remains the last safety net: our comparison of the best WordPress backup plugins helps you pick the tool.

For developers: from post functions to the CRUD API

Declaring compatibility

A plugin declares its compatibility in its main file, during the before_woocommerce_init action, as the official developer guide shows.

add_action( 'before_woocommerce_init', function () {
	if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
	}
} );

Passing false as the third argument declares incompatibility instead. Outside the main file, replace __FILE__ with the plugin path, such as my-plugin/my-plugin.php.

Replacing get_post and update_post_meta

Every order read goes through wc_get_order(), never get_post(). Metadata is written with the order object’s methods, followed by a save. The guide points out that save() is expensive and should not be called needlessly.

$order = wc_get_order( $order_id );
$order->update_meta_data( '_my_tracking', $tracking_number );
$order->save();

To find out which storage is active, the OrderUtil class provides custom_orders_table_usage_is_enabled(). To test an object’s type, it replaces get_post_type() with get_order_type().

Admin screens, columns and boxes

Under HPOS, the order list moves to admin.php?page=wc-orders, and the single order screen takes a new format. Boxes added to the order screen must target the screen ID returned by wc_get_page_screen_id( 'shop-order' ), and list columns use dedicated hooks, such as manage_woocommerce_page_wc-orders_columns.

New stores and the default choice

A developer who delivers new stores can influence the initial choice. The woocommerce_enable_hpos_by_default_for_new_shops filter, available since version 8.2, decides whether an install with no orders starts on HPOS. Better to leave it on and fix the plugins that block it, since HPOS is where the store will have to live.

Querying orders

The wc_get_orders() function accepts queries on metadata, order fields and dates, modeled on WP_Query. It works with both storages, which makes it the safe path for code meant to last, with one caveat: queries on order fields, added with HPOS, are only supported under HPOS.

What performance gains to expect

What the official figures really measure

WooCommerce announces order creation up to five times faster, checkout up to one and a half times faster, and order search up to forty times faster. These figures come from a March 2023 benchmark, on a development build, with about four hundred thousand orders and a single process.

The forty-fold gain concerns the customer filter, on an indexed column. Metadata search gained about tenfold, search on a non-indexed column about threefold. These measurements give orders of magnitude obtained under specific conditions, which your store will not necessarily reproduce.

What followed, and what the field says

HPOS keeps receiving its own optimizations. Since WooCommerce 10.4, an order cache has left the experimental phase, off by default and recommended alongside an object cache. In 10.7, one orders API route went from 271 to 132 queries per call on HPOS orders.

The Universal Yums account is nuanced: shortly after the migration, Devin Price saw no noticeable gain on an already optimized store, and a year later, he said HPOS made things much faster and let the team remove its workarounds. To really speed up checkout, other levers matter as much, as our guide to WooCommerce checkout optimization shows.

Summary table

Step Setting or command Risk Check
Inventory wp wc hpos compatibility-info Undeclared plugin, custom code Incompatible plugins list
Staging Production copy, synchronization Underestimated duration Measured sync time
Synchronization wp wc hpos sync Low, can be interrupted count_unmigrated at zero
Switch HPOS authoritative, compatibility on Instant rollback possible Full checkout journeys
End of transition Compatibility mode off Slower rollback wp wc hpos status
Cleanup wp wc hpos cleanup Hard rollback Verified backup beforehand

Frequently asked questions

Is HPOS mandatory?

No. Existing stores are not migrated automatically, and WooCommerce has announced no date for removing the legacy storage. It is nonetheless the reference storage for the future, and new optimizations target it.

Will my old orders move by themselves?

Only if you turn on synchronization. Background jobs then copy orders in batches, or the wp wc hpos sync command does it faster on the command line.

Can I go back?

Yes. With compatibility mode on, rolling back is instant. Without it, the legacy tables have to be rebuilt first, and after a cleanup, every order has to be copied back from HPOS.

Does compatibility mode slow down the store?

While the legacy tables are authoritative, the official guide saw no noticeable negative effect. Once HPOS is authoritative, double writing has a cost: WooCommerce says turning compatibility mode off can improve performance. Turn it off once the switch is validated.

What if a single plugin is incompatible?

Ask its vendor for an update, look for a replacement, or have its code adapted. Forcing activation despite the warning exposes you to incomplete orders or missing data.

Conclusion

HPOS finally stores orders where they belong, in tables designed for them. The migration is not automatic for an existing store, and that is a good thing: it calls for an inventory of plugins and custom code, a test run on a copy, a timed synchronization, then a switch with a way back.

Universal Yums showed that a store with more than ten million records can get there, and that surprises rarely come from where you expect them. Keep compatibility mode as long as needed, and only run the cleanup once confidence is established.

Sources


LaFactory designs, builds and maintains WordPress and WooCommerce sites, and develops its own plugins. Talk to us about your WordPress project.

Francis Rozange

Former section editor at Libération, he runs LaFactory, an international web agency since 1996.

Cart