WooCommerce

WooCommerce HPOS Migration Checklist: Test, Break, Roll Back

A working checklist for moving a live WooCommerce store to HPOS: what breaks, the queries to grep for, how to test on staging, and how to roll back.

5 min read
WooCommerceHPOSCheckout
870 words5 min read

Why this checklist exists

High-Performance Order Storage moves orders out of the posts and postmeta tables into dedicated tables. On a store with tens of thousands of orders, that is the single biggest admin and reporting speed-up available. It is also the migration that generates most of our WooCommerce rescue work, because anything that reads orders the old way keeps working right up until the moment it does not.

We have migrated enough stores, and fixed enough migrations done by others, to have a checklist. This is it.

Before you touch anything

Take a full backup you have tested restoring. Database and files. HPOS keeps the old tables during sync mode, but a failed migration on a live store is not the moment to discover your backup tool skips large tables.

Inventory the custom code. Search the theme, mu-plugins and any custom plugin for direct order access. The patterns to grep for:

Shell
grep -rnE "get_post_meta\(\s*\\\$order|get_post_meta\(\s*\\\$order_id|update_post_meta\(\s*\\\$order" wp-content/themes wp-content/plugins --include="*.php"
grep -rnE "post_type\s*=>\s*'shop_order'|'shop_order'" wp-content/themes wp-content/plugins --include="*.php"
grep -rnE "WP_Query\(|get_posts\(" wp-content/themes --include="*.php" | grep -i order

Every hit is a candidate for breakage. get_post_meta( $order_id, '_billing_email' ) returns nothing under HPOS because the meta is no longer in postmeta. WP_Query with post_type => 'shop_order' returns nothing because orders are no longer posts.

Inventory the plugins. Check every plugin's HPOS compatibility declaration. WooCommerce lists incompatible plugins in the settings screen and will not let you enable HPOS while one is active. Payment gateways, subscription and membership plugins, shipping integrations and analytics exporters are the usual suspects.

Rewriting the code

The replacements are mechanical once you know them.

Order data access:

PHP
// Before
$email = get_post_meta( $order_id, '_billing_email', true );
update_post_meta( $order_id, '_my_plugin_flag', 'yes' );

// After
$order = wc_get_order( $order_id );
$email = $order->get_billing_email();
$order->update_meta_data( '_my_plugin_flag', 'yes' );
$order->save();

Order queries:

PHP
// Before
$orders = get_posts( [
  'post_type'   => 'shop_order',
  'post_status' => 'wc-processing',
  'numberposts' => 50,
] );

// After
$orders = wc_get_orders( [
  'status' => 'processing',
  'limit'  => 50,
] );

Hooks that assumed orders are posts, such as save_post_shop_order or manage_shop_order_posts_custom_column, have HPOS-aware equivalents: woocommerce_process_shop_order_meta and manage_woocommerce_page_wc-orders_custom_column. Register both during the transition so the code works in either mode.

The rule that avoids nearly all of it: never touch order storage directly. Go through the WC_Order object and wc_get_orders(). Code written that way has been HPOS-compatible since before HPOS existed.

The staging run

  1. Clone the live store to staging with a recent database.
  2. Enable HPOS with compatibility mode (sync both tables) in WooCommerce settings, Advanced, Features.
  3. Run the sync and wait for it to finish. On a large store this takes a while; the tool reports progress.
  4. Test the flows that matter, in this order: place an order with each active gateway; refund one; change an order's status in admin; search orders by email and by order number; open WooCommerce Analytics and confirm revenue is not zero; run any export or reporting your business depends on; trigger a subscription renewal if you have subscriptions.
  5. Check the error log. Deprecation notices about post meta on orders point at code you missed.
  6. Only then, switch off compatibility mode on staging and repeat step 4.

The failures we see most

Analytics shows zero revenue. The lookup tables that Analytics reads were not regenerated after the switch. Regenerate them from WooCommerce, Status, Tools, or with WP-CLI: wp wc admin_import_orders. The orders are not lost. This is the most common panic call we get.

A gateway stops appearing at checkout. The gateway plugin declared incompatibility, or its availability check reads order meta the old way. Update the plugin first; if it is still broken, the vendor's changelog will say whether HPOS is supported.

Order search returns nothing for emails. Custom search code queried postmeta. Replace with wc_get_orders( [ 'billing_email' => $email ] ).

Bulk status updates fail on some orders. Usually a custom action hooked on save_post that throws under HPOS. Move it to the order-object hooks.

Duplicate order numbers or missing sequential numbers. A sequential-order-number plugin that stores its counter in postmeta. Check for an HPOS-compatible version before migrating.

Going live

Choose a quiet window. Enable HPOS in compatibility mode on production, run the sync, and leave compatibility mode on for a week. Both tables are kept in step, so anything you missed still works, and the error log tells you what it was. When a week passes clean, switch compatibility mode off.

If something goes badly wrong during that week, rolling back is a settings change: switch the authoritative source back to posts. Because compatibility mode kept both tables synced, nothing is lost. That is why the week matters and why skipping compatibility mode to save time is the wrong trade.

After the migration

Remove the shop_order post type assumptions from any remaining code, drop the compatibility hooks once you no longer need dual-mode support, and enjoy an orders screen that loads in under a second on a hundred thousand orders.

If you would rather not run this on a store that is taking money, that is the work we do: staging first, a rollback path before anything ships, and the gateways tested against real sandboxes.

All writingHire me for this