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.
Muhammad Qasim
Senior Full Stack Developer
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:
PHP Notice: Function WC_Order was called incorrectly.
Order properties should not be accessed directly.
And the bigger one:
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:
// 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:
$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:
// 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():
$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:
$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:
WooCommerce → Status → Logs → fatal-errors
And the CLI audit if you prefer:
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:
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.
Need Help With This?
I offer professional web development services — WordPress, React/Next.js, performance optimization, and technical SEO.
Get in Touch