Migrate WooCommerce Orders to High-Performance Order Storage (HPOS)

WooCommerce High-Performance Order Storage (HPOS), introduced as an opt-in feature in WooCommerce 7.1 and made the default in WooCommerce 8.2, replaces the legacy strategy of storing orders as WordPress posts in wp_posts and wp_postmeta with dedicated wc_orders, wc_order_addresses, wc_order_operational_data, and wc_orders_meta tables. The primary motivation is performance: wp_postmeta queries for orders at scale require multi-join operations across millions of rows shared with product, page, and attachment meta — HPOS tables are normalized for order-specific access patterns and indexed on order status, customer ID, date, and total. WooCommerce provides a built-in migration tool at WooCommerce → Settings → Advanced → Features that syncs existing orders from the post tables to the new order tables in batches; the sync can be paused and resumed and shows a progress bar with estimated time remaining. During the sync period, WooCommerce can run in compatibility mode (synchronizing writes to both storage layers) — this keeps third-party plugins that still read directly from wp_posts working without data loss while they are updated. A plugin is HPOS-compatible when it exclusively uses the WC_Order CRUD API (wc_get_order(), $order->get_meta(), $order->update_meta_data()) instead of get_post_meta() / update_post_meta() with a post ID. Declaring HPOS compatibility from a plugin requires calling FeaturesUtil::declare_compatibility() — WooCommerce will warn store owners in the admin if any active plugin has not declared compatibility. Direct SQL queries against wp_posts or wp_postmeta for orders will silently return stale or empty data once synchronization is disabled, so any custom reporting or migration script must be updated to use the wc_get_orders() query API or the HPOS tables directly. Order search, bulk status changes, and order list performance are measurably faster after migration on stores with more than 10,000 orders because wc_orders uses covering indexes on the columns most frequently used in admin queries. The rate-limiting post shows the transient-based hooks that interact with orders at checkout — all those hooks call WC CRUD methods and are fully HPOS-compatible without modification.

Problem: Stores with tens of thousands of orders suffer slow order list queries because orders share the wp_posts and wp_postmeta tables with all other post types, and third-party plugins that bypass the WC CRUD API will break silently when HPOS is enabled.

Solution: Enable HPOS in WooCommerce settings, run the built-in migration with compatibility mode on, audit plugins that use get_post_meta() on order IDs, replace them with $order->get_meta(), and declare HPOS compatibility once the plugin is fully migrated.

// 1. Declare HPOS compatibility from your plugin
use Automattic\WooCommerce\Utilities\FeaturesUtil;

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

// 2. Replace legacy post meta calls with WC CRUD
// BEFORE (breaks with HPOS):
$tracking = get_post_meta($order_id, '_tracking_number', true);
update_post_meta($order_id, '_tracking_number', $new_value);

// AFTER (HPOS-compatible):
$order = wc_get_order($order_id);
if ($order) {
    $tracking = $order->get_meta('_tracking_number', true);
    $order->update_meta_data('_tracking_number', $new_value);
    $order->save();
}

// 3. Replace WP_Query order lookups with wc_get_orders()
// BEFORE:
$query  = new WP_Query(['post_type' => 'shop_order', 'post_status' => 'wc-processing', 'posts_per_page' => -1]);
$orders = $query->posts; // returns WP_Post objects

// AFTER:
$orders = wc_get_orders([
    'status' => 'processing',
    'limit'  => -1,
    'return' => 'objects', // returns WC_Order objects
]);

// 4. Custom aggregate reporting directly on HPOS tables
global $wpdb;
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT o.id, o.status, o.total_amount, o.date_created_gmt,
                oa.first_name, oa.last_name, oa.email
         FROM {$wpdb->prefix}wc_orders o
         LEFT JOIN {$wpdb->prefix}wc_order_addresses oa
               ON oa.order_id = o.id AND oa.address_type = 'billing'
         WHERE o.status = %s
           AND o.date_created_gmt >= %s
         ORDER BY o.date_created_gmt DESC
         LIMIT 100",
        'wc-processing',
        gmdate('Y-m-d H:i:s', strtotime('-30 days'))
    )
);

NOTE: Do not disable synchronization (compatibility mode) until every active plugin on your store declares HPOS compatibility — WooCommerce shows an incompatibility warning for each plugin that has not called FeaturesUtil::declare_compatibility(). Disabling sync with an incompatible plugin active will cause that plugin to read stale order data from wp_posts instead of wc_orders.