← All posts Insights 5 min read

WooCommerce HPOS Is Now Default: Migrating Your NetSuite Sync Code Before 10.0

WooCommerce 9.8 made High-Performance Order Storage the default. Seven places it silently breaks legacy NetSuite integrations, with the code patterns to migrate each.

WooCommerce 9.8 (March 2026) made High-Performance Order Storage the only supported path for new installs and pushed the legacy posts-table sync into deprecation. If your NetSuite integration was written before mid-2024, there’s a good chance it’s still reading orders from wp_posts and wp_postmeta. It probably still works. It also probably won’t survive the 10.0 release later this year.

What this post covers: a checklist of every place HPOS quietly changes the contract for a NetSuite sync, with the code patterns to migrate each one. Written from the migration we’ve done across 14 mid-market stores in the last 18 months.

What HPOS actually changed

Until HPOS, every WooCommerce order was a custom post type. Order line items lived in wp_woocommerce_order_items; everything else — totals, billing address, shipping, payment method, refund references — was rows in wp_postmeta. It worked, but querying it at any scale was painful.

HPOS splits orders into four dedicated tables: wp_wc_orders, wp_wc_order_addresses, wp_wc_order_operational_data, and wp_wc_orders_meta. Reads are 30–50× faster on large stores. The catch is that direct SQL against wp_posts for orders no longer returns anything once compatibility-mode sync is disabled — which is the default in 9.8.

The seven places integrations break

1. Direct wp_posts queries in the cron worker

The classic pattern — “every five minutes, find orders with status processing and no _netsuite_synced meta, push to NetSuite” — is almost always implemented as a raw SQL join against wp_posts and wp_postmeta. After HPOS, that query returns zero rows.

// Before (breaks under HPOS)
$order_ids = $wpdb->get_col("
  SELECT p.ID FROM {$wpdb->posts} p
  LEFT JOIN {$wpdb->postmeta} m
    ON p.ID = m.post_id AND m.meta_key = '_netsuite_synced'
  WHERE p.post_type = 'shop_order'
    AND p.post_status = 'wc-processing'
    AND m.meta_id IS NULL
  LIMIT 50
");

// After
$order_ids = wc_get_orders([
  'status'     => 'processing',
  'limit'      => 50,
  'return'     => 'ids',
  'meta_query' => [[
    'key'     => '_netsuite_synced',
    'compare' => 'NOT EXISTS',
  ]],
]);

The HPOS-safe path goes through wc_get_orders() and the CRUD layer. Slower per call than raw SQL, but it’s the only API that survives both backends.

2. get_post_meta() on order IDs

Anywhere your code does get_post_meta($order_id, '_netsuite_id', true) needs to become $order->get_meta('_netsuite_id'). The post-meta call returns null on HPOS stores. WooCommerce does not warn you.

3. The save_post hook

Many integrations trigger a sync on save_post_shop_order. That hook no longer fires on HPOS stores. Replace with:

add_action('woocommerce_after_order_object_save', function($order) {
  // push to NetSuite
});

// And for status changes specifically:
add_action('woocommerce_order_status_changed', function($order_id, $from, $to, $order) {
  // ...
}, 10, 4);

4. Refund handling

Refunds used to be child posts of the order. Under HPOS they’re rows in wp_wc_orders with type = 'shop_order_refund' and a parent_order_id column. If your NetSuite credit-memo flow walks get_children(), swap it for $order->get_refunds().

5. Custom order statuses

Custom statuses still work, but the wc- prefix handling changed. The wp_wc_orders.status column stores the unprefixed value (awaiting-ns, not wc-awaiting-ns). Code that filters statuses by string prefix needs adjusting.

6. Bulk meta updates

“Mark these 5,000 orders as synced” with a single SQL UPDATE wp_postmeta is gone. The HPOS meta table is wp_wc_orders_meta, but you should not touch it directly — its schema is allowed to change. Use the data store: WC_Data_Store::load('order')->update_meta() in a batched job.

7. Reporting and admin filters

Custom admin columns and filters added via manage_edit-shop_order_columns only render on the legacy screen. The HPOS orders screen uses woocommerce_shop_order_list_table_columns. If your NetSuite plugin adds a “Sync status” column for ops, you’ll need both filters for a transition period.

⚠ The silent-failure trap: WooCommerce can run in “compatibility mode” where both tables stay in sync. It’s a migration aid, not a permanent home. Every store we’ve seen leave it on permanently eventually hits a race condition where the legacy table is hours behind. Migrate the code, then turn compatibility off.

A safe migration order

  1. On a staging clone, enable HPOS with compatibility mode on. Run a full daily sync. Diff the NetSuite side against production — you’ll catch most of the API mismatches here.
  2. Replace every get_post_meta on order IDs with $order->get_meta(). Grep for it; this is the single most common bug.
  3. Move cron workers to wc_get_orders(). Benchmark — they’ll be slower per query but more predictable.
  4. Replace save_post hooks with the WooCommerce equivalents.
  5. Turn compatibility mode off in staging. Run another full sync cycle. If it still passes, schedule production.
  6. In production: enable HPOS with compatibility on, observe for 7 days, then disable compatibility.

The cost of waiting

WooCommerce 10.0 is on the roadmap for Q4 2026 and the project has said the legacy storage will be removed in a subsequent release. The integrations we’ve migrated proactively took 2–4 weeks of focused work. The ones we’ve migrated under pressure — store offline, NetSuite out of sync, finance ringing — have taken twice that and cost real revenue.

If your NetSuite ↔ WooCommerce code was written before mid-2024 and nobody has audited it for HPOS, that audit is the highest-leverage hour of engineering work you’ll do this quarter.


Ship it

Need this in your stack?

We build, integrate, and ship — no calls, just delivery.

Start a project →