HPOS and NetSuite Sync Code: What Is Actually True in 2026
- There is no deadline. WooCommerce has never announced a version or date for removing legacy posts-table order storage, and none exists as of August 2026.
- HPOS is the default only for stores launched on or after 10 October 2023 (WooCommerce 8.2). Older stores stay on posts-table storage until an admin switches them.
- The real breaking change shipped in WooCommerce 10.7 on 14 April 2026: sync on read is now off by default, so order data written straight to
wp_postmetano longer gets pulled into the HPOS tables on the next read. - Consequence for integrations: sync code using
get_post_meta()or$wpdbagainst the posts tables does not throw. It reads empty strings and writes into a table WooCommerce has stopped consulting.
Most guidance on High-Performance Order Storage is organised around a migration deadline that WooCommerce has never set. Checking the primary sources instead of the summaries changes the whole shape of the problem: there is no cut-off to race, but there is a specific change, in a specific version, that turned a large class of already-broken integration code from silently-corrected into silently-wrong. That change is the removal of sync on read in WooCommerce 10.7, and it is the reason a NetSuite sync that worked for two years can start writing orders into a table nobody reads.
This guide states what WooCommerce has actually committed to, what 10.7 changed, which sync operations diverge, how to detect divergence that has already happened, and the order to migrate in when an ERP integration is live in production.
Contents
- There is no announced removal date for legacy order storage
- HPOS is default only for stores launched on or after 10 October 2023
- What WooCommerce 10.7 changed: sync on read is off by default
- Which sync operations diverge, and how each one fails
- Why the failure is silent instead of loud
- Where order data actually lives under HPOS
- The five patterns to replace in NetSuite sync code
- save_post no longer fires for orders
- How to detect divergence that already happened
- The migration sequence when the ERP integration is live
- Declare compatibility, or the feature stays disabled
- What to pin in the integration test matrix
There is no announced removal date for legacy order storage
WooCommerce has not published a version number or a date for removing posts-table order storage. The rollout announcement commits only to the direction: “Eventually, all Woo stores will migrate to HPOS. When we’re ready to migrate existing stores over, we will reach out again to notify our existing stores not currently on HPOS.” No sunset version appears in the developer documentation, the merchant documentation, or the rollout post.
This matters because the deadline framing produces the wrong plan. A team that believes it has until a named release schedules a single cutover and treats the interval as safe. A team that knows there is no deadline can instead sequence the work by risk: fix the code that reads and writes order data first, migrate storage second, and disable compatibility mode last, with verification between each step. The absence of a deadline removes the time pressure, not the defect.
Treat any content that names a specific removal version as unverified until it cites a WooCommerce release post that says so. Release-note claims about this platform decay quickly and are frequently restated wrongly across secondary sources, a pattern worth applying to every breaking-change claim in the WooCommerce store operations guide library.
HPOS is default only for stores launched on or after 10 October 2023
HPOS became stable and default for new installations in WooCommerce 8.2, released October 2023. WooCommerce states the split by launch date rather than by version: “If your store was launched before October 10, 2023, you’ll be able to enable HPOS within your WooCommerce settings. If you’ve launched your store on or after October 10, 2023, great news — HPOS is automatically enabled for your store!”
For integration work, the install date decides which failure mode applies. A store launched in 2024 has been HPOS-authoritative since day one, so posts-table sync code never worked there and the breakage was visible immediately. A store launched in 2021 and upgraded through every release since is still on posts-table storage unless someone changed the setting, and its sync code still works — until the storage setting moves.
The dangerous middle case is a store that enabled HPOS with compatibility mode left on. Both tables are populated, the integration appears to work, and the defect stays hidden behind synchronisation. Until 10.7, one half of that synchronisation was doing more work than most teams realised.
What WooCommerce 10.7 changed: sync on read is off by default
WooCommerce 10.7, released 14 April 2026, disabled HPOS sync on read by default. Sync on read was the mechanism that quietly repaired writes made outside the CRUD layer. WooCommerce describes exactly what it did: “If an operation wrote order data directly to the posts table, bypassing WooCommerce, the HPOS tables would get out of sync. To handle this, WooCommerce compared the update timestamps of both records during read operations and, if the post record was more recent, pulled those changes back into HPOS.”
That comparison is what kept non-compliant integrations correct on HPOS stores running compatibility mode. Remove it, and a write to wp_postmeta stays in wp_postmeta. WooCommerce reads the HPOS tables, finds the older value, and serves it to the admin screens, the REST API, emails, and every downstream consumer.
The affected population is specific: stores relying on “custom code or plugins that write order data directly to the posts table (via wp_update_post, update_post_meta, or direct SQL) and expect those changes to be reflected in HPOS orders on the next read.” Affected stores get a one-time dismissible admin notice after upgrading — which is a notice about a class of code, not a report of which orders have already diverged.
Which sync operations diverge, and how each one fails
Four sync operations account for most of the divergence in an ERP integration, and each fails differently. The table maps the legacy call to its HPOS-safe replacement and to the symptom the store actually reports, which is rarely phrased as a storage problem.
| Sync operation | Legacy call | HPOS-safe call | Symptom after 10.7 |
|---|---|---|---|
| Write the ERP order ID back onto the order | update_post_meta($id, '_netsuite_so_id', $so) |
$order->update_meta_data() then $order->save() |
Orders re-export on every run; duplicate sales orders in the ERP |
| Read the ERP order ID to decide whether to export | get_post_meta($id, '_netsuite_so_id', true) |
$order->get_meta('_netsuite_so_id') |
Empty string reads as “never exported”; the guard never fires |
| Select orders changed since the last run | $wpdb query over wp_posts |
wc_get_orders() with date_created |
Zero rows returned; the batch silently exports nothing |
| Flag an order as fulfilled from the ERP | wp_update_post() plus post meta |
$order->set_status() then $order->save() |
Admin and customer emails keep showing the pre-fulfilment status |
Verdict: the first two rows are the expensive ones. A guard that reads empty and a write that lands nowhere combine into an export loop that re-sends the same order indefinitely, which is the duplicate-sales-order incident that shows up as an ERP data-quality problem rather than a WooCommerce one.
Why the failure is silent instead of loud
The failure is silent because every call involved is still valid PHP against a table that still exists. get_post_meta() on an HPOS store returns an empty string rather than raising an error, because the function did its job — it looked in wp_postmeta and found nothing. There is no exception, no deprecation notice, and no failed HTTP status for a monitor to catch.
Writes are worse than reads. A read that returns empty at least produces visibly wrong behaviour somewhere downstream. A write to wp_postmeta succeeds, returns a truthy value, and gets logged by the integration as a successful write. The integration’s own logs will report a clean run while the store shows stale data.
This is why the 10.7 change deserves a code audit rather than a monitoring rule. There is no error to alert on. The only reliable signal is a comparison between the two storage locations, which is the detection step below.
Where order data actually lives under HPOS
HPOS replaces two general-purpose WordPress tables with four dedicated order tables. Any query written against the posts tables needs to know which of the four now holds the field it wanted.
| Data | Legacy storage | HPOS storage |
|---|---|---|
| Order record | wp_posts (post_type = shop_order) |
wp_wc_orders |
| Order meta | wp_postmeta |
wp_wc_orders_meta |
| Billing and shipping addresses | wp_postmeta, prefixed keys |
wp_wc_order_addresses |
| Operational data (totals, dates, flags) | wp_postmeta |
wp_wc_order_operational_data |
| Line items | wp_woocommerce_order_items |
wp_woocommerce_order_items, unchanged |
Verdict: read-only reporting queries can target these tables directly, but the schema is internal and version-dependent, so it is not a substitute for the CRUD layer in sync code. Writes go through the order object in both storage modes, without exception.
The five patterns to replace in NetSuite sync code
Five call patterns cover nearly every HPOS defect in integration code. Each replacement works in both storage modes, so the migrated code is safe to deploy before the storage setting changes.
// 1. Loading an order — never get_post()
$order = wc_get_order( $order_id );
if ( ! $order ) {
return; // deleted, or not an order ID
}
// 2. Reading order meta
$ns_order_id = $order->get_meta( '_netsuite_so_id' );
// 3. Writing order meta — save() is required, the setter alone does not persist
$order->update_meta_data( '_netsuite_so_id', $so_internal_id );
$order->save();
// 4. Selecting orders to export
$orders = wc_get_orders( [
'status' => [ 'processing', 'on-hold' ],
'date_created' => '>=' . $since, // '>=YYYY-MM-DD'
'limit' => 100,
'orderby' => 'date',
'order' => 'ASC',
'return' => 'objects',
] );
// 5. Type checks — never get_post_type()
use AutomatticWooCommerceUtilitiesOrderUtil;
if ( OrderUtil::is_order( $id, wc_get_order_types() ) ) {
// it is an order in either storage mode
}
The save() call in pattern 3 is the one most often missed in a hurried migration. update_meta_data() mutates the in-memory object only; without save() the value is discarded at the end of the request, which reproduces the original bug through new API calls.
Unbounded queries are the other trap. wc_get_orders() accepts 'limit' => -1, and an export job that uses it will load every order into memory on a store that has grown since the code was written. Page with limit and an ID or date cursor instead.
save_post no longer fires for orders
Under HPOS, orders are not WordPress posts, so save_post and save_post_shop_order do not fire for them. Any integration that triggers an ERP export from save_post stops exporting entirely the moment the store becomes HPOS-authoritative — a loud failure compared with the silent ones above, but one that is easy to misattribute to the ERP side.
// Fires in both storage modes
add_action( 'woocommerce_new_order', 'sx_export_order_to_erp', 10, 1 );
add_action( 'woocommerce_update_order', 'sx_export_order_to_erp', 10, 1 );
function sx_export_order_to_erp( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
// Idempotency guard: the ERP ID is the key, not a local "synced" flag
if ( $order->get_meta( '_netsuite_so_id' ) ) {
return;
}
// ... export, then write the returned ID back with update_meta_data() + save()
}
Two behaviours differ from the posts-table equivalents and both affect export volume. HPOS triggers woocommerce_update_order more often than the posts-table implementation triggered save_post, so any webhook or export attached to it fires more frequently and needs the idempotency guard shown above rather than an assumption of one call per change. Separately, an auto-draft order does not trigger woocommerce_new_order until it is first saved with an order status, so code expecting a hook at row-creation time will not get one.
Admin-screen code moves too: metabox registration keys off wc_get_page_screen_id( 'shop-order' ) instead of the post screen, and metabox callbacks receive a WC_Order rather than a WP_Post. Handle both while the store can still be switched back.
How to detect divergence that already happened
Divergence is detected by comparing the two storage locations for the same order, because no error was ever raised at write time. WooCommerce ships a command for exactly this comparison, and it is the first thing to run on any store that has been on HPOS with compatibility mode through the 10.7 upgrade.
# Compare legacy posts-table order data against the HPOS tables
wp wc hpos verify_data
# Re-sync the orders that differ
wp wc hpos verify_data --re-migrate
Note the command name before scripting it: WooCommerce’s CLI reference documents wp wc hpos verify_data, while its own large-store migration guide still shows the older verify_cot_data spelling. Run wp help wc hpos against the installed version and use what that reports.
The command tells you which orders differ, not which of your own meta keys caused it. To narrow the audit to the integration’s own keys, compare the two meta tables directly for the keys the sync code owns — a read-only query, safe on production, that answers whether the integration is the source of the drift.
SELECT p.post_id AS order_id,
p.meta_value AS posts_table_value,
o.meta_value AS hpos_value
FROM wp_postmeta p
JOIN wp_wc_orders_meta o
ON o.order_id = p.post_id
AND o.meta_key = p.meta_key
WHERE p.meta_key = '_netsuite_so_id'
AND p.meta_value <> o.meta_value
LIMIT 100;
Rows returned mean the integration wrote to the posts table and the write never reached HPOS. Zero rows with a non-empty posts table means the keys match; zero rows with an empty result on both sides means the key is not in use under either storage mode, which is its own finding.
The migration sequence when the ERP integration is live
The order of operations matters more than the speed. Code correctness comes first, because migrating storage under non-compliant code converts a hidden defect into a production incident. WooCommerce’s own guidance for large stores follows the same shape: enable synchronisation with posts authoritative, migrate, verify, then disable sync in stages rather than all at once.
According to WooCommerce’s large-store migration guide, a test store of 9 million orders took about a week to complete its migration — plan the window from a staging run against a copy of production data, not from an estimate.
- Grep the integration for
get_post_meta,update_post_meta,delete_post_meta,get_post,wp_update_post,WP_Querywithshop_order, and direct SQL againstwp_postsorwp_postmeta. Every hit is either replaced or documented as deliberate. - Replace each hit with the CRUD equivalent, and confirm every
update_meta_data()is followed bysave(). - Move export triggers off
save_postontowoocommerce_new_orderandwoocommerce_update_order, with an idempotency guard keyed on the ERP record ID. - Declare HPOS compatibility in the integration so the feature can be enabled at all.
- Deploy the migrated code to production while the store is still posts-authoritative, and run a full order cycle: create, pay, export, fulfil, refund.
- On staging, restore a copy of production, enable compatibility mode with posts authoritative, and run the sync to completion. Time it.
- Run
wp wc hpos verify_dataand resolve every difference before switching authority. - Switch to HPOS authoritative with compatibility mode still on, and run the full order cycle again against the live ERP sandbox.
- Disable sync on read first, then sync on write, verifying the order cycle after each step.
- Leave the legacy tables in place until a full reporting period has passed. Cleanup is irreversible and buys nothing operationally.
Declare compatibility, or the feature stays disabled
WooCommerce disables the HPOS option when an active plugin has not declared compatibility with it, so an undeclared integration blocks the migration for the whole store regardless of whether its code is correct. The declaration is a single hook, registered on before_woocommerce_init.
add_action( 'before_woocommerce_init', function () {
if ( class_exists( AutomatticWooCommerceUtilitiesFeaturesUtil::class ) ) {
AutomatticWooCommerceUtilitiesFeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
} );
Declaring compatibility is an assertion, not a test — WooCommerce takes the plugin at its word. Declare it after the code audit, never before, or the store loses the one guard rail that was stopping an unsafe migration.
To branch behaviour at runtime while both modes are in play, detect the active mode rather than inferring it: OrderUtil::custom_orders_table_usage_is_enabled() returns whether HPOS is authoritative. Use it for logging and diagnostics, not to maintain two parallel data paths — one CRUD path that works in both modes is the goal.
What to pin in the integration test matrix
An HPOS test matrix needs four storage configurations, because the defects surface in different ones. Testing only the endpoint state hides every divergence bug, since a store with HPOS authoritative and sync fully off will fail loudly rather than silently.
| Configuration | Authoritative | Catches |
|---|---|---|
| Legacy only | Posts tables | Regressions in the pre-migration state the store is in today |
| Compatibility mode, posts authoritative | Posts tables | Sync failures during the migration window |
| Compatibility mode, HPOS authoritative | HPOS tables | Silent divergence — the 10.7 failure mode |
| HPOS only, sync off | HPOS tables | Remaining posts-table reads and writes, loudly |
Verdict: the third row is the one to add if only one configuration can be afforded, because it is the state most upgraded stores are actually in and the only one where the failure produces no error. Pin the WooCommerce version in the matrix too — a suite that passed on 10.6 proves nothing about 10.7 behaviour, which is the same staging discipline that WooCommerce release upgrades demand of integrators.
The pattern generalises beyond storage. A platform default changes, the old path keeps working for a while through a compatibility shim, and the shim is removed a few releases later — which is exactly how customisations built for the classic checkout shortcode stopped applying once stores moved to the block checkout. Deciding which system owns each write, as covered in the guide to webhooks versus polling for inventory sync, is what keeps a shim removal from becoming an outage.
Auditing an existing NetSuite integration against this list, or replacing one that still reads orders through post meta, is work SoftXone does directly — the NetSuite Integration Pro order sync runs on the CRUD layer in both storage modes.
Get the working checklists
The runbooks and decision checklists from these guides, as printable PDFs — free in the SoftXone guide library.
References
- HPOS sync on read to be disabled by default in WooCommerce 10.7WooCommerce Developer Blog — the advisory defining what sync on read did, which stores are affected, and the
woocommerce_hpos_enable_sync_on_readfilter. - High-Performance Order StorageWooCommerce developer docs — the four order tables, stable-since version, and compatibility-mode behaviour.
- Platform Upgrade: High-Performance Order StorageWooCommerce.com — the 10 October 2023 launch-date split, and the absence of any removal date for legacy storage.
- HPOS extension recipe bookWooCommerce developer docs — compatibility declaration,
OrderUtildetection, and the admin screen-ID changes. - HPOS CLI toolsWooCommerce developer docs —
wp wc hpos sync,verify_data, andcleanupwith their flags. - A large store’s guide to enable HPOSWooCommerce developer docs — staged sync disabling, verification, and the 9-million-order migration timing.
- wc_get_orders() and order queriesWooCommerce developer docs — supported query arguments and the date comparison syntax.
Frequently asked questions
Is there a WooCommerce version that removes legacy posts-table order storage?
No. WooCommerce has published no version number and no date for removing it, and the rollout announcement commits only to notifying existing stores when it is ready to migrate them. Watch the developer advisories feed rather than the release notes, since the last storage-behaviour change arrived as an advisory two months before the release it shipped in. Plan the migration around code readiness, not around a calendar date that does not exist.
Does sync on read still exist after WooCommerce 10.7?
Yes, but only as an opt-in. The woocommerce_hpos_enable_sync_on_read filter turns it back on, and WooCommerce describes re-enabling it as a temporary measure because sync on read may be removed completely in a future version. Treat the filter as a window in which to fix integration code, not as a setting to leave on. Code that depends on it is code that will break again on an unannounced schedule.
How do I check whether my store is HPOS-authoritative right now?
In the admin, WooCommerce > Settings > Advanced > Features shows which storage is selected and whether compatibility mode is enabled. In code, OrderUtil::custom_orders_table_usage_is_enabled() returns true when HPOS is authoritative. Check both, because the configuration that hides divergence is HPOS-authoritative with compatibility mode still populating the posts tables. A store in that state looks healthy from either check taken alone.
Can I roll back to the posts tables after switching to HPOS?
Yes, provided compatibility mode stayed enabled and the legacy rows were never deleted. The storage setting flips back and the posts tables are still populated. Once the cleanup command has run, the legacy order rows are gone and rollback is no longer possible. That is why cleanup belongs after a full reporting period has closed, not inside the migration window where it looks like the last step.
Does the WooCommerce REST API return different order data under HPOS?
No. The REST API reads through the same CRUD layer as the admin screens, so an external integration that pulls orders over wc/v3 is unaffected by the storage change. The exposure is limited to code running inside WordPress that reaches past the CRUD layer to the posts tables. An integration split between REST calls and in-process hooks can therefore pass every API test while its hook code writes to a table nothing reads.

Leave a Reply