The Problem
A client running a 40k order WooCommerce store hit a weird one this week. They selected 200 orders in WooCommerce → Orders, picked Change status to completed from the bulk action dropdown, and got the green "200 orders updated" notice. Refresh the page and every single order was still Processing. No PHP error, no JavaScript error in the console, no entry in the WooCommerce log. The success banner was a clean lie.
The store had been migrated to High-Performance Order Storage (HPOS) about three weeks earlier with sync enabled, and the bulk action used to work fine. The trigger turned out to be a custom plugin the previous developer had bolted on to push completed orders into a third-party fulfilment system. The bulk update was running, the plugin's hook was firing on the legacy post status, and HPOS was getting written separately. Sometimes.
Single order status changes from the order edit screen worked perfectly. It was bulk only.
Why It Happens
When HPOS is the authoritative store and post sync is enabled, WooCommerce keeps two copies of every order: the row in wp_wc_orders (the source of truth) and the legacy wp_posts row tagged as shop_order for backwards compatibility. Most of the core admin code was rewritten to talk to HPOS first. The bulk actions handler is one of the places where custom code written in the pre-HPOS era still goes wrong.
The plugin on this site was hooking into bulk_actions-edit-shop_order. That filter only ever fires on the legacy post list table. Under HPOS the bulk handler runs through Automattic\WooCommerce\Internal\Admin\Orders\ListTable::process_bulk_action(), which dispatches a different action. The plugin's filter callback was overwriting the status update array with the orders it considered "ready to fulfil", silently dropping the rest, and then bailing without returning anything that the new HPOS code path could use. The HPOS table never got the write. The legacy post table did, then sync ran on the next request and overwrote the change back to Processing because HPOS is authoritative.
That is why the UI flashed green: the legacy handler completed. And that is why the change vanished: HPOS reversed it on the next sync tick. The HPOS plugin compatibility guide calls this out, but only if you go looking. There is no deprecation warning. The hook simply does nothing useful on HPOS-enabled stores.
The Fix
There are two things to fix here. First, stop the custom plugin from corrupting the bulk update path. Second, write the status change through the HPOS-aware API so it actually persists.
Step 1: Replace the legacy filter with the HPOS-compatible action. The old code looked like this:
// Old, fires on legacy post list only.
add_filter('bulk_actions-edit-shop_order', function ($actions) {
// ...the plugin was mutating $actions and the status list here.
return $actions;
});
The HPOS equivalent runs on a different screen ID and uses woocommerce_ prefixed hooks. Swap the filter for the action that fires after a single order is updated, so the plugin reacts to status changes instead of trying to intercept the bulk dispatch:
add_action('woocommerce_order_status_changed', function ($order_id, $from, $to, $order) {
if ($to !== 'completed') {
return;
}
// Push to the fulfilment system. This runs for both single
// and bulk updates because the bulk handler loops and fires
// this action per order on HPOS.
fulfilment_push_order($order);
}, 10, 4);
This hook fires once per order regardless of whether the status change came from the order edit screen, a bulk action, or the REST API. You stop trying to filter the bulk dispatch and let WooCommerce do its job.
Step 2: If you have custom bulk actions, register them the HPOS way. Use the new filter and handler:
add_filter('bulk_actions-woocommerce_page_wc-orders', function ($actions) {
$actions['mark_fulfilled'] = __('Mark fulfilled', 'my-plugin');
return $actions;
});
add_filter(
'handle_bulk_actions-woocommerce_page_wc-orders',
function ($redirect_to, $action, $ids) {
if ($action !== 'mark_fulfilled') {
return $redirect_to;
}
$updated = 0;
foreach ($ids as $id) {
$order = wc_get_order($id);
if (! $order) {
continue;
}
$order->update_status('completed', 'Bulk fulfilled via admin.');
$updated++;
}
return add_query_arg('bulk_action_updated', $updated, $redirect_to);
},
10,
3
);
Two things matter here. The screen ID is woocommerce_page_wc-orders, not edit-shop_order. And the status is set with $order->update_status(), which writes through HPOS and triggers woocommerce_order_status_changed for any other plugin listening.
Step 3: Verify the write is sticking. Run the bulk action on five test orders, then query the HPOS table directly:
SELECT id, status FROM wp_wc_orders WHERE id IN (1001, 1002, 1003, 1004, 1005);
The status column should read wc-completed for each one. If it does, run the same check against the legacy table:
SELECT ID, post_status FROM wp_posts WHERE ID IN (1001, 1002, 1003, 1004, 1005);
Both should match. If HPOS reads wc-completed and wp_posts reads wc-processing, sync is lagging and you can force it under WooCommerce → Status → Tools → Update database. If HPOS still reads wc-processing after the bulk action, a plugin is still hooking the legacy bulk filter and short-circuiting the update.
The Lesson
HPOS does not warn you when a plugin is still listening on legacy bulk action hooks. The UI lies, the sync layer cleans up, and the change disappears. Move custom code to woocommerce_order_status_changed for reactions and to the wc-orders screen ID for custom bulk actions, and let WooCommerce route writes through HPOS itself.
If your store has been on HPOS for a while and you are quietly losing status changes on bulk operations, that is the exact kind of legacy plugin audit I run for clients. See my services. For a related HPOS gotcha on the order search, read WooCommerce HPOS order search by email not working.
Bulk updates lying to your admins? Let me fix it.
