Writing WooCommerce Order Code that Works with High‑Performance Order Storage
Learn how to refactor WooCommerce order code to use the CRUD API so it works whether High‑Performance Order Storage is enabled or not.
26 Jul 2026, 11:49 UTC

Why Direct Post Queries Break under HPOS
For many years WooCommerce stored each order as the shop_order custom post type, with its data spread across wp_posts and wp_postmeta. Custom code that fetched an order with get_post() or read metadata via get_post_meta() worked reliably as long as the store used the legacy tables. When a store enables High‑Performance Order Storage (HPOS) the order rows move to dedicated tables (wp_wc_orders and related meta tables). Queries that still target wp_posts return empty results, and any raw SQL that joins those tables silently fails.
Using the CRUD API for Single Order Access
The supported, storage‑agnostic way to read or modify an order is through the WooCommerce CRUD API. The function wc_get_order($order_id) returns a WC_Order object regardless of which storage engine is active. Its getter methods (get_billing_email(), get_total(), get_meta(), etc.) read from the correct table set behind the scenes.
Below is a before‑and‑after snippet that shows the refactor for retrieving a billing email.
// Legacy approach – breaks when HPOS is enabled
$order_id = 123;
$post = get_post($order_id);
$email = get_post_meta($order_id, '_billing_email', true);
if ($email) {
echo 'Sending mail to: ' . esc_html($email);
}
// HPOS‑compatible approach – works in both storages
$order_id = 123;
$order = wc_get_order($order_id);
if ($order) {
$email = $order->get_billing_email();
echo 'Sending mail to: ' . esc_html($email);
}
Listing Orders with wc_get_orders()
When you need a list of orders that match certain criteria, replace WP_Query with wc_get_orders(). The function accepts the same familiar arguments (status, customer, date_created, limit, orderby, order) and internally routes the query to the active order data store.
$orders = wc_get_orders([
'status' => 'wc-completed',
'customer' => 45,
'limit' => 10,
'orderby' => 'date',
'order' => 'DESC',
]);
foreach ($orders as $order) {
// $order is a WC_Order object
echo $order->get_order_id() . ': ' . $order->get_total() . '
';
}
Declaring HPOS Compatibility
WooCommerce can block plugins that have not declared compatibility with the custom order tables. To avoid being flagged, add a declaration on the before_woocommerce_init hook.
add_action('before_woocommerce_init', function () {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
});
Place this code in your main plugin file; it tells WooCommerce that your extension has been tested with HPOS enabled.
Trade‑offs and Verification Steps
While HPOS improves read performance, its synchronization mode (used during migration) writes to both the legacy and the new tables, roughly doubling the cost of each order create/update operation. This is intentional as a temporary state; once migration is complete you can disable sync to regain the write‑speed benefit.
To verify that your refactored code works in both modes:
- On a staging site, toggle HPOS on and off via WooCommerce → Settings → Advanced → Features.
- Create a test order, note its billing email and total.
- Run your code (or the snippets above) and confirm the output matches the known values in both storage modes.
- Search your codebase for
shop_order,get_post_meta, or rawwp_postsjoins and replace each occurrence with the appropriate CRUD call.
By adopting the storage‑agnostic pattern, you ensure that your extensions remain functional today and continue to work as WooCommerce evolves its order data layer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.