WooCommerce HPOS compatibility means a plugin reads and saves orders through WooCommerce’s own order methods and declares that it does. HPOS has been the default order storage on new stores since WooCommerce 8.2. A plugin that still reads orders from the posts tables can break on a fresh install, which makes it not HPOS compatible.
We tested every step on a WooCommerce 11.2.0 test store running WordPress 7.1.3 in October 2026. We also read the WooCommerce documentation and the HPOS extension recipe book for each claim, and we ran the code samples in this guide.
Below you will learn what HPOS is and how to check its status and your plugins. Then you will see what breaks when a plugin ignores it, how to declare compatibility in code, and how to migrate and sync orders.
What Is WooCommerce HPOS?
HPOS stands for High-Performance Order Storage. It keeps orders in dedicated database tables instead of the WordPress posts and postmeta tables. WooCommerce used to call it Custom Order Tables, and its developer documentation says it improves scalability, reliability and simplicity for stores of every size.
Before version 8.2, WooCommerce stored orders in the posts tables. The WooCommerce documentation lists the four tables that now hold order data: wc_orders, wc_order_addresses, wc_order_operational_data and wc_orders_meta.

That sentence matters for existing stores. HPOS is on by default only for new installations. The same documentation says the feature is opt-in for stores that already run, and you can switch back at any time.
Here is how the two storage options differ in practice. We read each value below on our test store.

How to Check HPOS Status in WooCommerce
You can check HPOS status in two places. Open WooCommerce > Settings > Advanced > Features and read the Order data storage option. The system status report shows the same answer in its Settings section. You can paste it to a support team.
Step 1: Open the Features Screen
Go to WooCommerce > Settings > Advanced and click the Features link. The Order data storage row shows the active option. High-performance order storage is selected on our test store, and the compatibility mode checkbox below it is ticked.
Compatibility mode keeps the posts tables in step with the HPOS tables. Leave it on while you test and while older plugins still read the posts tables.

Step 2: Read the System Status Report
Open WooCommerce > Status. In the Settings section, three rows answer the question. HPOS enabled shows a green check, Order datastore names the OrdersTableDataStore class, and HPOS data sync enabled shows whether compatibility mode is on.

Check HPOS From the Command Line
One WP-CLI command prints the HPOS status: run wp wc hpos status. It reports whether HPOS and compatibility mode are on, how many orders are out of sync, and how many are waiting for cleanup. It works well on a server where you have no browser.
This is the output on our test store:
wp wc hpos status HPOS enabled?: yes Compatibility mode enabled?: yes Unsynced orders: 0 Orders subject to cleanup: 0
Each line has a plain meaning. Unsynced orders counts orders whose changes have not reached the other set of tables yet. Orders subject to cleanup counts orders that still hold redundant data in the postmeta table after a migration.
The command wp wc hpos cleanup exists for that second case. Its help text says it removes redundant data from the postmeta table for migrated orders when HPOS is enabled. Run it only after a verified sync, and keep a backup.
The WooCommerce documentation also names a command called wp wc cot sync. On WooCommerce 11.2 that old name still exists. Its migrate command is marked as deprecated with a note to use wp wc hpos sync instead. Use the hpos commands.
How to Check Plugin Compatibility With HPOS
WooCommerce checks your active plugins when HPOS is on and when you try to switch. A plugin that declares itself incompatible blocks the switch, as the WooCommerce documentation explains. We also found that a plugin that says nothing about HPOS gets flagged too.
An old plugin can therefore show a warning even though its author never wrote anything about it.
Step 1: Look for the Incompatible Plugin Warning
Open the same Features screen. If a plugin conflicts, WooCommerce prints a line such as “1 Incompatible plugin detected” under the HPOS option and names the plugin. We activated a test plugin with no HPOS declaration, and this is what appeared.

Step 2: Open the Incompatible Plugins List
Click View and manage under that message. WordPress opens the Plugins screen filtered to the plugins incompatible with the High-Performance order storage feature. The WooCommerce documentation gives the same screen as a URL with the feature ID custom_order_tables.

Step 3: Read the Plugins Screen Notice
The main Plugins screen shows a red notice at the top while a conflict exists. It says WooCommerce found active plugins that are incompatible with the features currently enabled. Its link, review the details, opens a Plugins list filtered to the incompatible plugins.

Step 4: See What Blocks the Switch
If you run posts storage and a plugin declares itself incompatible, WooCommerce disables the HPOS option. We tested that with a plugin that calls the declaration with false. The radio button is greyed out, and the same warning names the plugin.

The command line gives the same verdict. wp wc hpos compatibility-info groups the plugins into compatible, incompatible and uncertain. On our store, the plugin that declared support was compatible, and the plugin that said nothing was uncertain:
wp wc hpos compatibility-info 1 compatible plugin found: Order Notes Example 0 incompatible plugins found 1 uncertain plugin found: Undeclared Order Example
When a plugin is incompatible, WooCommerce asks you to contact its developer. You can keep working on posts storage in the meantime. The WooCommerce documentation says an extension that is not compatible keeps working when you do not enable HPOS.
What Breaks When a Plugin Ignores HPOS?
A plugin that is not HPOS compatible reads orders with get_post_meta() or direct SQL. Once HPOS is active it finds nothing, unless compatibility mode copies the data back. The recipe book says direct reads may return an outdated order, and direct writes may update an order nobody reads.
We created one order with a billing email and a custom field on our test store. Then we read it back both ways. On HPOS without compatibility mode, the CRUD methods returned both values and get_post_meta() returned an empty string for both:
HPOS in use: yes CRUD billing email: [buyer@example.com] CRUD custom meta: [Happy birthday] get_post_meta billing email: [] get_post_meta custom meta: [] rows in wc_orders: 1 shop_order rows in wp_posts: 0
This is the failure that makes HPOS bugs hard to spot. There is no error. The plugin simply sees an empty value and carries on. A coupon rule or an export can quietly stop working.
With compatibility mode on, WooCommerce copied the same order into the posts tables, and the get_post_meta() calls returned the values again. That is why compatibility mode helps during a migration. It does not make the plugin compatible.
| Check on our test store | HPOS only | HPOS with compatibility mode |
|---|---|---|
| Order methods read the value | Yes | Yes |
| get_post_meta() reads the value | No | Yes |
| shop_order rows in wp_posts | 0 | 1 |
Replace Direct Order Queries With the Order API
Use wc_get_order() to load an order and its methods to read and write data. Then call save() once. WooCommerce works out which tables are active and writes to the right ones. The same code runs on HPOS and on posts storage.
The recipe book gives a search expression to audit your code. It lists WordPress post functions such as get_post, get_post_meta and update_post_meta, plus wpdb and the shop_order post type. Expect many false matches, and check each one that touches an order.
The common replacements look like this:
// Instead of get_post( $order_id ) and get_post_meta(). $order = wc_get_order( $order_id ); $email = $order->get_billing_email(); $note = $order->get_meta( '_ddx_gift_note' ); // Instead of update_post_meta(). $order->update_meta_data( '_ddx_gift_note', 'Happy birthday' ); $order->save();

The recipe book also warns that save() is an expensive call. Call it once per flow when you can, and avoid extra calls on the same order object.
To check whether an ID is an order, use OrderUtil in place of get_post_type(). On our test store OrderUtil::get_order_type() returned shop_order and OrderUtil::is_order() returned yes for our test order:
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( OrderUtil::is_order( $id, wc_get_order_types() ) ) {
// This ID is an order, whichever storage is active.
}
Query Orders With WC_Order_Query
Use WC_Order_Query or wc_get_orders() instead of WP_Query with the shop_order post type. They query the active storage. One call works on HPOS and on posts storage. The recipe book says HPOS adds query types for dates, metadata and order fields.
One detail cost us time on the test store. A meta_query condition worked on HPOS and was silently ignored on posts storage.
We saved a note on three orders, and only order 36 held the note we searched for. The same query returned all three orders after we switched to posts storage.
$query = new WC_Order_Query( array( 'limit' => 10, 'return' => 'ids', 'meta_key' => '_ddx_gift_note', 'meta_value' => 'Happy anniversary', ) ); $order_ids = $query->get_orders();
The meta_key and meta_value form returned only order 36 on both storages. Use it while your plugin supports both. Switch to meta_query only when you require HPOS, and test the query on both modes before you ship.
Raw SQL is the one place where you need to know the storage. The recipe book offers OrderUtil::custom_orders_table_usage_is_enabled() for that. It returns true when HPOS is active. You can pick the right table for a query that you cannot write with the order API.
Show and Save Fields on the Order Screen
HPOS uses its own order edit screen. A meta box has to target that screen. Register the box on wc_get_page_screen_id( ‘shop-order’ ) when HPOS is active and on shop_order otherwise. The callback receives either a post or an order object. Normalize it first.
We tested this code on the HPOS order screen. It shows a text field in the sidebar, checks a nonce and the edit capability on save, and stores the value through the order object. WooCommerce 11.2.0 fires the woocommerce_process_shop_order_meta action with the order ID when you click Update, and our save function listens to it:
add_action( 'add_meta_boxes', 'ddx_add_gift_note_box' );
function ddx_add_gift_note_box() {
$screen = OrderUtil::custom_orders_table_usage_is_enabled()
? wc_get_page_screen_id( 'shop-order' )
: 'shop_order';
add_meta_box( 'ddx_gift_note', 'Gift note', 'ddx_render_gift_note_box', $screen, 'side', 'high' );
}
function ddx_render_gift_note_box( $post_or_order_object ) {
$order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
if ( ! $order ) {
return;
}
wp_nonce_field( 'ddx_save_gift_note', 'ddx_gift_note_nonce' );
printf( '<input type="text" name="ddx_gift_note" class="widefat" value="%s">', esc_attr( $order->get_meta( '_ddx_gift_note' ) ) );
}
add_action( 'woocommerce_process_shop_order_meta', 'ddx_save_gift_note' );
function ddx_save_gift_note( $order_id ) {
if ( ! isset( $_POST['ddx_gift_note_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['ddx_gift_note_nonce'] ) ), 'ddx_save_gift_note' ) ) {
return;
}
if ( ! current_user_can( 'edit_shop_order', $order_id ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( $order && isset( $_POST['ddx_gift_note'] ) ) {
$order->update_meta_data( '_ddx_gift_note', sanitize_text_field( wp_unslash( $_POST['ddx_gift_note'] ) ) );
$order->save();
}
}
We typed a note and clicked Update on order 36. The value stayed in the field. A read through the order methods then returned the same text.
The field is plain text on purpose. If you store anything richer, such as a list or a date, sanitize it with the function that matches the type. Never save raw request data on an order.

How to Declare WooCommerce HPOS Compatibility
WooCommerce HPOS compatibility is a declaration your plugin makes with FeaturesUtil::declare_compatibility() on the before_woocommerce_init hook. You pass the custom_order_tables feature ID and your plugin file. Declare it only after you have audited and tested the plugin, because the declaration is your promise to store owners.
The recipe book says to put this code in your main plugin file. Note the leading backslashes on the class name, which matter inside a namespaced file:
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 );
}
}
);

The class_exists() check keeps the code from failing on a WooCommerce version without that class. Pass false as the last argument if you know the code does not support HPOS. If the declaration lives outside the main file, pass the plugin slug path, such as my-plugin-slug/my-plugin.php, in place of __FILE__.
If your declaration code should run only when WooCommerce is present, learn how to check if a plugin is active first.
The recipe book adds a detail that explains why some plugins never show a warning. WooCommerce only shows compatibility information for plugins whose header has a “WC tested up to” line. Add that header so the Plugins screen can report on your plugin.
A declaration is not a test. Keep your own checks from the steps above.
Our WooCommerce Tiered Pricing plugin does the same thing in its main file. It declares custom_order_tables and cart_checkout_blocks as true on before_woocommerce_init, using the same class_exists() check.
For a full plugin from scratch, see our guide to creating a WooCommerce plugin.
Migrate Orders and Keep Them in Sync
To move an existing store to HPOS, turn on compatibility mode first and let the sync finish. Then switch the storage option. The WooCommerce documentation says to keep plugins that use custom post types active during the switch. Deactivating them first can leave order data behind.
You can run the sync from the Features screen or from the command line. We ran these commands on our test store. The first one copied one order that we had created before turning compatibility mode on:
wp wc hpos sync There is 1 order to be synced. Sync completed. Success: 1 order was synced in 0 seconds. wp wc hpos verify_data Verification completed. Success: 3 orders were verified in 0 seconds.
Order matters. We tried to switch the authoritative table while one order was out of sync. WooCommerce refused with a message that the table cannot change while orders are out of sync.
The developer documentation states the same rule. After the sync, the switch worked.
The enable command also checks your plugins. With an incompatible plugin active, wp wc hpos enable stopped and told us to review the plugins on the Features screen. After we deactivated that plugin, the command printed Success: HPOS enabled.
- wp wc hpos status: Shows whether HPOS and sync are on and how many orders are unsynced.
- wp wc hpos sync: Copies orders between the posts tables and the HPOS tables.
- wp wc hpos verify_data: Compares the migrated data with the original posts data.
- wp wc hpos disable: Switches back to posts storage when the pre-checks pass.
If a problem appears after the switch, the documentation says to return to WordPress posts storage temporarily. Keep compatibility mode on and wait for the sync to finish first. A guide like our plugin conflict check helps you find the plugin behind it.
Stores with heavy order volume can have more to plan than a clean test store. A WooCommerce development team can audit the custom code that touches orders before you switch.
Conclusion
WooCommerce HPOS compatibility comes down to three habits. Read and write orders through the order methods, test with HPOS on and off, and declare the result in your main plugin file.
Store owners can check the Features screen, the status report or wp wc hpos status in a minute. A plugin that shows a warning needs its developer to act, and you can stay on posts storage until it does.
If you run SQL against orders, our guide on marking WooCommerce orders complete with MySQL shows why the table matters.
Frequently Asked Questions (FAQs)
Q1. Is HPOS enabled by default in WooCommerce?
Yes for new installations, since WooCommerce 8.2. The WooCommerce documentation says existing stores are not migrated automatically. They can switch from WooCommerce, Settings, Advanced, Features when they choose.
Q2. Do I have to turn on HPOS?
No. The WooCommerce documentation says the feature is opt-in for existing stores, and you can switch back at any time. Turn it on after your plugins show as compatible and a test run passes.
Q3. What does compatibility mode do?
It copies order changes between the HPOS tables and the posts tables. Older code that reads get_post_meta() keeps working while it is on. On our test store, a read through get_post_meta() returned the value only with the mode on.
Q4. What does the incompatible plugin warning mean?
WooCommerce found an active plugin that declared no support or declared incompatibility. On our test store, a plugin that said nothing was also flagged. Contact the developer, or deactivate the plugin before you switch.
Q5. How do I test my plugin for HPOS compatibility?
Run your flows on a test store with HPOS on, with HPOS off, and with compatibility mode on. Create orders, edit them, and run exports and queries each time. Then check that no warning appears on the Features screen.
Q6. Can I switch back from HPOS to posts storage?
Yes. Turn on compatibility mode, wait for the sync to finish, and select WordPress posts storage. The documentation recommends this when an incompatible plugin must keep working, and wp wc hpos disable runs the same pre-checks.
