WooCommerce High-Performance Order Storage (HPOS) complete technical reference. Covers database schema (wp_wc_orders, wp_wc_order_addresses, wp_wc_order_operational_data, wp_wc_orders_meta), CRUD API migration from wp_posts/wp_postmeta to WC_Order objects, OrdersTableDataStore, plugin compatibility declaration via FeaturesUtil, synchronization mechanics, CLI tools (wp wc hpos), and order querying APIs (meta_query, field_query, date_query). Use this skill whenever the user's code involves WooCommerce orders, order storage, order metadata, order queries, wc_get_order, wc_get_orders, WC_Order, WC_Order_Query, custom_order_tables, OrderUtil, FeaturesUtil::declare_compatibility, or any migration from post-based order storage to HPOS custom tables. Also use when the user mentions HPOS, COT (Custom Order Tables), order tables, order data store, or order synchronization in a WooCommerce context. This skill replaces the need to search the web for HPOS documentation.
Since: WooCommerce 8.2 (Oct 2023, default for new installs) | Source: developer.woocommerce.com | Updated: 2026-03-15
HPOS replaces the legacy wp_posts + wp_postmeta order storage with four dedicated tables optimized for eCommerce queries. Performance gains: up to 5x faster order creation, 1.5x faster checkout, 40x faster order lookups. Order INSERTs drop from ~40 to at most 5 per order.
| Table | Purpose |
|---|---|
wp_wc_orders | Primary order data (status, currency, totals, customer, payment, dates) |
wp_wc_order_addresses | Billing and shipping addresses (one row per address type per order) |
wp_wc_order_operational_data | Internal state flags (created_via, order_key, stock_reduced, email_sent, etc.) |
wp_wc_orders_meta | Extension/custom metadata (replaces wp_postmeta for orders) |
Order notes remain in wp_comments. Order items retain their dedicated tables. See references/database-schema.md for full column definitions.
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// HPOS is active
} else {
// Legacy CPT storage
}
// Get order -- replaces get_post()
$order = wc_get_order( $order_id );
// Read data
$status = $order->get_status();
$total = $order->get_total();
$email = $order->get_billing_email();
// Update metadata -- replaces update_post_meta()
$order->update_meta_data( '_custom_key', 'value' );
$order->save(); // Required! Expensive -- batch changes before calling.
// Delete metadata -- replaces delete_post_meta()
$order->delete_meta_data( '_custom_key' );
$order->save();
use Automattic\WooCommerce\Utilities\OrderUtil;
// replaces: 'shop_order' === get_post_type( $id )
'shop_order' === OrderUtil::get_order_type( $id );
// replaces: in_array( get_post_type( $id ), wc_get_order_types() )
OrderUtil::is_order( $id, wc_get_order_types() );
add_action( 'before_woocommerce_init', function() {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables', __FILE__, true
);
}
} );
Pass false as third arg to declare incompatibility. Use 'my-plugin-slug/my-plugin.php' instead of __FILE__ if the declaration is outside the main plugin file.
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
add_action( 'add_meta_boxes', function() {
$screen = class_exists( CustomOrdersTableController::class )
&& wc_get_container()->get( CustomOrdersTableController::class )
->custom_orders_table_usage_is_enabled()
? wc_get_page_screen_id( 'shop-order' )
: 'shop_order';
add_meta_box( 'my-box', 'Title', 'render_callback', $screen, 'side', 'high' );
} );
function render_callback( $post_or_order ) {
$order = ( $post_or_order instanceof WP_Post )
? wc_get_order( $post_or_order->ID )
: $post_or_order;
// Use $order exclusively below this point
}
// Basic query (works on both backends)
$orders = wc_get_orders( array(
'status' => 'processing',
'limit' => 50,
'orderby' => 'date',
'order' => 'DESC',
) );
// HPOS-enhanced: meta_query
$orders = wc_get_orders( array(
'meta_query' => array(
array( 'key' => 'color', 'compare' => 'EXISTS' ),
array( 'key' => 'size', 'value' => 'small', 'compare' => 'LIKE' ),
),
) );
// HPOS-enhanced: field_query
$orders = wc_get_orders( array(
'field_query' => array(
'relation' => 'OR',
array( 'field' => 'total', 'value' => '5.0', 'compare' => '<' ),
array( 'field' => 'shipping_total', 'value' => '5.0', 'compare' => '<' ),
),
) );
// HPOS-enhanced: date_query
$orders = wc_get_orders( array(
'date_query' => array(
array( 'column' => 'date_paid_gmt', 'after' => '1 month ago' ),
),
) );
See references/querying-apis.md for full query type documentation with advanced examples.
When HPOS is enabled with compatibility mode, data is kept in sync between the new tables (authoritative) and legacy posts tables (backup).
| Setting | Authoritative | Backup | Behavior |
|---|---|---|---|
| HPOS on, sync on | wp_wc_orders | wp_posts | Dual writes, immediate sync |
| HPOS on, sync off | wp_wc_orders | None (placeholders only) | Full performance, no fallback |
| Posts on, sync on | wp_posts | wp_wc_orders | Legacy mode with async backfill |
| Posts on, sync off | wp_posts | None | Classic WooCommerce behavior |
Key option: woocommerce_custom_orders_table_enabled (true = HPOS active).
Sync option: woocommerce_custom_orders_table_data_sync_enabled.
Placeholder records: When HPOS is authoritative and sync is disabled, creating an order inserts a post with type shop_order_placehold to reserve the ID. This ensures post.ID == order.id invariant.
Conflict resolution (sync on read): Compares timestamps. If CPT update_time < HPOS update_time, assumes failed CPT write and overwrites CPT. If timestamps match, assumes direct CPT write and syncs to HPOS.
| Command | Purpose |
|---|---|
wp wc hpos status | Overview of HPOS settings and sync state |
wp wc hpos enable [--with-sync] | Enable HPOS (optionally with compatibility mode) |
wp wc hpos disable | Disable HPOS (must sync pending orders first) |
wp wc hpos sync | Migrate/sync orders between datastores |
wp wc hpos count_unmigrated | Count orders pending sync |
wp wc hpos verify_data [--re-migrate] | Verify consistency between datastores |
wp wc hpos diff <order_id> | Show differences for a specific order |
wp wc hpos backfill <id> --from=<posts|hpos> --to=<posts|hpos> | Copy order data between stores |
wp wc hpos cleanup <id|range|all> [--force] | Remove legacy data after migration |
See references/cli-tools.md for full command documentation with examples.
For large stores, follow the 3-phase approach documented in references/migration-guide.md:
wp wc hpos sync, benchmark migration time, verify with wp wc hpos verify_data --verbose, audit third-party systems accessing DB directly.get_post() / update_post_meta() for orders -- these bypass HPOS and write to wrong tables. Always use wc_get_order() and $order->update_meta_data().$order->save() after metadata changes -- but minimize calls (batch changes first). save() is expensive.wp_posts for orders will break -- use wc_get_orders() or WC_Order_Query. If you must write SQL, check OrderUtil::custom_orders_table_usage_is_enabled() and query the correct table.Search your codebase with these regex patterns to find code that needs HPOS migration:
Direct DB/post access:
wpdb|get_post|get_post_field|get_post_status|get_post_type|get_posts|get_post_meta|update_post_meta|add_post_meta|delete_post_meta|wp_insert_post|wp_update_post|wp_delete_post|wp_trash_post|shop_order
Admin screen functions:
post_updated_messages|do_meta_boxes|enter_title_here|edit_form_before_permalink|manage_shop_order_posts_columns|manage_shop_order_posts_custom_column
Most matches will be false positives. Check each for order-related usage.
$synchronizer = wc_get_container()->get(
Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::class
);
$order_ids = $synchronizer->get_next_batch_to_process( $batch_size );
if ( count( $order_ids ) ) {
$synchronizer->process_batch( $order_ids );
}
add_filter( 'woocommerce_hpos_enable_sync_on_read', '__return_false' );
Apply this filter 6+ hours after switching HPOS to authoritative. It reduces overhead while maintaining write sync for safety.
| Need | File |
|---|---|
| Full database table schemas and column definitions | references/database-schema.md |
| Complete CLI command reference with examples | references/cli-tools.md |
| Advanced order querying (meta_query, field_query, date_query) | references/querying-apis.md |
| Step-by-step migration guide for large stores | references/migration-guide.md |
| Plugin compatibility patterns and code migration recipes | references/plugin-compatibility.md |