WooCommerce / HPOS
WooCommerce HPOS: Common Problems With Custom Code
Short answer
With HPOS authoritative, orders live in wp_wc_orders and related tables, not in wp_posts and wp_postmeta. Code that reads or writes orders through post functions, WP_Query or SQL on the posts tables stops working — often silently, returning empty values. Rewrite it with the order API (wc_get_order(), wc_get_orders(), $order->get_meta(), $order->update_meta_data() plus $order->save()), update admin hooks for the new orders screen, and declare HPOS compatibility in your plugins.
Symptoms
- Order meta read by custom code comes back empty after HPOS is enabled.
- Custom columns, filters or meta boxes are missing from the new orders screen.
- Exports, reports or integrations stop including new orders.
- Values saved by custom code don't appear on the order, or disappear after the next save.
- WooCommerce shows a notice that active plugins are incompatible and won't let you switch to HPOS.
Most common causes
| Pattern | Why it breaks |
|---|---|
get_post_meta( $order_id, '_my_key', true ) |
Reads wp_postmeta, which is not the source of truth with HPOS |
update_post_meta( $order_id, … ) |
Writes to the posts storage; HPOS never sees it, or sync overwrites it |
new WP_Query( [ 'post_type' => 'shop_order' ] ) / get_posts() |
Queries wp_posts, where new orders may not exist |
$wpdb queries on wp_posts / wp_postmeta for orders |
Hard-coded legacy schema |
wp_insert_post() / wp_update_post() on orders |
Bypasses the order data stores |
manage_edit-shop_order_columns only |
Legacy admin screen hooks; the HPOS screen uses different ones |
add_meta_box( …, 'shop_order' ) |
Wrong screen id for the HPOS edit screen |
Diagnosis
1. Check which storage is authoritative
WooCommerce → Settings → Advanced → Features shows whether orders are stored in HPOS tables or legacy posts tables, and whether compatibility mode keeps both in sync. While sync is on, legacy code may appear to work for reading, which hides the problem until sync is turned off.
2. Search the code
Search the theme, custom plugins, mu-plugins and snippet plugins:
$ grep -rnE "get_post_meta|update_post_meta|delete_post_meta|wp_insert_post|wp_update_post" wp-content/themes/my-theme wp-content/plugins/my-plugin
$ grep -rnE "shop_order|wp_posts|wp_postmeta|post_type *=" wp-content/themes/my-theme wp-content/plugins/my-pluginEach hit needs a decision: does it touch orders? Product and page code is unaffected.
3. Check extensions
WooCommerce lists incompatible plugins on the Features settings screen. For each, check whether an update declares compatibility; if not, ask the vendor or plan a replacement.
4. Test on staging with sync on, then off
Enable HPOS on staging with compatibility mode, run the critical flows — checkout, payment notifications, emails, admin edits, exports, integrations — then switch synchronisation off and run them again. Code that only works with sync on is not compatible.
Logs and technical checks
- The Features screen: authoritative storage, sync status, incompatible plugins.
- WooCommerce → Status → Logs:
fatal-errorsafter switching storage. - Order notes and meta on test orders, compared before and after the switch.
- Read-only comparisons of the same order in both storages while sync is on.
Solutions
Read and write orders through the order API
// Read
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$po_number = $order->get_meta( '_po_number' );
// Write — changes are not stored until save() is called
$order->update_meta_data( '_po_number', sanitize_text_field( $value ) );
$order->save();
// Query
$orders = wc_get_orders( [
'status' => [ 'wc-processing' ],
'limit' => 50,
'meta_query' => [
[ 'key' => '_po_number', 'compare' => 'EXISTS' ],
],
] );Support both admin screens
use Automattic\WooCommerce\Utilities\OrderUtil;
// Columns: legacy screen and HPOS screen
add_filter( 'manage_edit-shop_order_columns', 'myplugin_add_column' );
add_filter( 'manage_woocommerce_page_wc-orders_columns', 'myplugin_add_column' );
add_action( 'manage_shop_order_posts_custom_column', function ( $column, $post_id ) {
myplugin_render_column( $column, wc_get_order( $post_id ) );
}, 10, 2 );
add_action( 'manage_woocommerce_page_wc-orders_custom_column', function ( $column, $order ) {
myplugin_render_column( $column, $order );
}, 10, 2 );
// Meta box on whichever order edit screen is active
add_action( 'add_meta_boxes', function () {
add_meta_box( 'myplugin-box', 'Purchase order', 'myplugin_render_box', wc_get_page_screen_id( 'shop-order' ), 'side' );
} );
function myplugin_render_box( $post_or_order ) {
$order = $post_or_order instanceof WP_Post ? wc_get_order( $post_or_order->ID ) : $post_or_order;
// …
}Declare compatibility in your own plugins
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 );
}
} );What not to do
- Do not declare compatibility to silence the warning before checking the code.
- Do not turn off compatibility mode right after switching: keep it as a rollback path until the store has run correctly for a while.
- Do not "fix" missing values by copying meta between tables with SQL.
When to contact an expert
Bring in an engineer when the audit finds order access spread across many files, when an essential extension has no compatible version, when order data already differs between storages, or when the store is large enough that the migration itself needs planning.
Related problems
- WooCommerce / Orders WooCommerce Orders Missing or Incorrect Before assuming an order is lost, check every status, drafts and the payment provider. Incorrect totals usually come from tax settings, fees, coupons or recalculation.
- WooCommerce / Errors WooCommerce Fatal Error: A Practical Troubleshooting Guide Read the fatal error properly — file, line, stack trace — and connect it to the change that triggered it.
- WooCommerce / Errors WooCommerce Plugin Conflict: How to Find the Cause Start from the evidence — the error, the timing, the request — and isolate on staging by halves, not one plugin at a time on production.
Frequently asked questions
How do I know whether my code is HPOS-compatible?
Search it for direct order access: get_post_meta and update_post_meta on order ids, WP_Query or get_posts with shop_order, wp_insert_post for orders, and SQL against wp_posts or wp_postmeta for orders. Code that only uses wc_get_order, wc_get_orders and order object methods is usually compatible.
Why does my custom column disappear from the orders list?
The HPOS orders screen is a different admin page with different hooks. Columns registered with manage_edit-shop_order_columns need the manage_woocommerce_page_wc-orders_columns equivalents as well.
Do I need to declare compatibility?
Yes, for your own plugins. WooCommerce uses the declaration to decide whether HPOS can be enabled. Declare it only after the code has been checked.
Can I still run SQL against orders?
Prefer wc_get_orders(). If you truly need SQL, get the table names from WooCommerce instead of hard-coding wp_posts, and remember the schema differs between storages.