WooCommerce

WooCommerce HPOS Custom Queries Breaking: The Fix

WooCommerce HPOS breaks custom order queries using WP_Query or postmeta. Here's the exact fix with working code for WooCommerce 8.7+ stores.

4 min read
WooCommerce HPOS Custom Queries Breaking: The Fix
WooCommerceWordPressHPOS
775 words4 min read

Problem

A client pinged me last week: after enabling High-Performance Order Storage (HPOS) on their WooCommerce 8.7 store, their custom order reports went blank. The dashboard widgets, the CSV export, the Slack notifications. Nothing pulled orders anymore. Fatal errors in the logs:

Text
PHP Notice: Function WC_Order was called incorrectly.
Order properties should not be accessed directly.

And the bigger one:

Text
Uncaught Error: Call to undefined method WC_Order::get_post_meta()

If you're reading this, you've probably just flipped the HPOS switch under WooCommerce → Settings → Advanced → Features and half your custom code exploded. Here's why, and how I fixed it.

Why It Happens

Before HPOS, WooCommerce stored orders as shop_order posts in wp_posts, with order data in wp_postmeta. That's how it worked for a decade, so every plugin, snippet, and custom report written for WooCommerce queried those tables directly.

HPOS moves order data into dedicated tables: wp_wc_orders, wp_wc_order_addresses, wp_wc_order_operational_data, and wp_wc_orders_meta. When you enable HPOS, the old postmeta rows are no longer the source of truth. They're either empty or stale depending on your sync setting.

So any code that does this is now broken:

PHP
// Broken on HPOS: returns nothing
$orders = new WP_Query([
    'post_type'   => 'shop_order',
    'post_status' => 'wc-completed',
    'meta_query'  => [
        [
            'key'   => '_billing_email',
            'value' => 'customer@example.com',
        ],
    ],
]);

WP_Query hits wp_posts. There are no orders there under HPOS. The query returns an empty set, your report goes blank, and your export silently ships a zero-row CSV.

The Fix

WooCommerce provides wc_get_orders() as the HPOS-safe equivalent. It abstracts the storage layer, so it works whether HPOS is on or off. Rewrite the query like this:

PHP
$orders = wc_get_orders([
    'status'         => 'completed',
    'billing_email'  => 'customer@example.com',
    'limit'          => -1,
    'return'         => 'objects',
]);

foreach ($orders as $order) {
    $total    = $order->get_total();
    $email    = $order->get_billing_email();
    $order_id = $order->get_id();
    // do stuff
}

A few gotchas I hit on the client site:

1. Don't access postmeta directly. If your code calls get_post_meta($order_id, '_billing_total', true), swap it for the order method:

PHP
// Before (breaks on HPOS)
$total = get_post_meta($order_id, '_billing_total', true);

// After (works on both)
$order = wc_get_order($order_id);
$total = $order->get_total();

2. Custom meta still works, but use the order API. If you've been storing custom fields like _qasim_gift_message on orders, read them via $order->get_meta():

PHP
$gift_message = $order->get_meta('_qasim_gift_message');
$order->update_meta_data('_qasim_gift_message', 'Happy birthday');
$order->save();

3. Complex SQL needs the new tables. If you had a raw $wpdb query joining wp_posts and wp_postmeta, you need to rewrite it against wp_wc_orders and wp_wc_orders_meta. For most cases, though, wc_get_orders() with a meta_query arg covers you:

PHP
$orders = wc_get_orders([
    'status'     => 'processing',
    'limit'      => 100,
    'meta_query' => [
        [
            'key'     => '_qasim_gift_message',
            'compare' => 'EXISTS',
        ],
    ],
]);

4. Check compatibility before enabling. WooCommerce shows an incompatible plugin warning on the HPOS settings page. Don't ignore it. I had a client whose affiliate plugin hadn't updated for HPOS, and enabling it dropped 3 months of referral data from the reports. Run this in the admin to audit:

Text
WooCommerce → Status → Logs → fatal-errors

And the CLI audit if you prefer:

Shell
wp wc hpos verify_cot_data --verbose

5. Turn on compatibility mode during the rollout. Under Advanced → Features, set data sync to Keep the posts table and orders tables synchronized. That way, if a plugin still reads from postmeta, it keeps working while you fix the calling code. Only turn sync off once every query uses the order API.

One More Thing. Scheduled Actions

If you use Action Scheduler jobs that grab orders (abandoned cart emails, subscription renewals, sync to an ERP), double-check the callback. Most of mine were built with get_posts('shop_order'). Every one of them broke silently.

Here's a safer pattern I now use as the default:

PHP
add_action('qasim_daily_order_sync', function () {
    $orders = wc_get_orders([
        'status'       => ['processing', 'completed'],
        'date_created' => '>' . (time() - DAY_IN_SECONDS),
        'limit'        => -1,
    ]);

    foreach ($orders as $order) {
        qasim_sync_order_to_erp($order);
    }
});

date_created accepts a timestamp prefix (>, <, or a range with ...). That's not documented well anywhere. the official HPOS developer guide is the closest thing.

For a deeper look at making WooCommerce stores fast at the infrastructure level, my WordPress performance optimization guide covers the caching and database work I pair with HPOS migrations.

Need a Hand With Your HPOS Migration?

I've migrated production WooCommerce stores to HPOS without losing a single order or report. If your store is stuck in compatibility mode or your custom plugin is throwing errors, book a quick audit on my services page and I'll tell you exactly what needs to change.

All writingHire me for this