Skip to content
All articles

Migrating a Busy WooCommerce Store to HPOS Without Downtime

8 min read

For most of its life WooCommerce kept orders in wp_posts and wp_postmeta — the same two tables as your blog posts, your pages and every attachment in the media library. An order was a post with a private post type, and every field on it, from the customer's email to the shipping method, was a row in postmeta. It worked. It also meant that "show me last month's unfulfilled orders over $200" was a set of joins against the single busiest table in the database, competing with everything else the site stores there.

High-Performance Order Storage gives orders their own tables, with columns and indexes designed for the queries a shop actually runs. The performance argument is not controversial and I won't spend long on it. What I want to write down is the part that goes wrong: the order of operations, and the two things people believe about HPOS that aren't true.

First, find out what you're actually on

WooCommerce's own documentation puts it precisely: from WooCommerce 8.2, released October 2023, HPOS is "officially released under the stable flag and will be enabled by default for new installations."

New installations. That is the whole trap. A store that existed before 8.2 and has been dutifully updated ever since is running WooCommerce 10-point- something and still storing orders in wp_posts, because an upgrade doesn't move your data — it would be reckless if it did. So the version number in your admin footer tells you nothing at all about which data store you're on, and I've watched people conclude they'd "already migrated" on exactly that basis.

Look at the setting instead: WooCommerce → Settings → Advanced → Features. Or check the woocommerce_custom_orders_table_enabled option directly. From code, WC_Data_Store::load( 'order' ) returns an OrdersTableDataStore when HPOS is authoritative and a WC_Order_Data_Store_CPT when the posts table still is.

Second: nobody has announced a deadline

There is a genre of post claiming legacy order storage is deprecated and will be removed at the end of 2026, or some other specific date. I went looking for the source of that when writing this, because if true it changes the urgency completely. It isn't in the developer documentation, which discusses both data stores as live options and gives no sunset date. I couldn't find it stated by WooCommerce anywhere.

I'd treat it as invented until someone links the announcement. That doesn't make migrating a bad idea — new features are being built against HPOS, and the admin speedup on a store with real order history is the kind you notice without a benchmark. But "do it because the queries are faster" and "do it before they delete your data" are very different arguments, and only the first one is supported.

The audit that has to happen first

The migration itself is well-trodden. What breaks is third-party code, and it breaks in a specific way: anything that reads orders with a direct database query against wp_posts or wp_postmeta will silently stop seeing orders once HPOS is authoritative and sync is off. Not error — stop seeing. An accounting export that quietly returns zero rows is a worse failure than a crash, because nothing alerts you.

So before anything else, go through every plugin that touches orders — accounting sync, shipping labels, ERP connectors, subscriptions, that reporting plugin nobody remembers installing — plus any custom code in the theme. You're looking for raw $wpdb queries and get_post_meta() calls against order IDs. The fix, where you own the code, is to go through the CRUD API instead: wc_get_orders(), $order->get_meta(), $order->save(). Those work against whichever store is authoritative, which is the entire point of them.

Where you don't own the code, the compatibility declaration is a decent signal — WooCommerce asks extensions to declare HPOS support, and the Features screen lists plugins that haven't. Treat that list as "needs testing", not as "will break", and treat an empty list as "needs testing" too, since a plugin can declare compatibility and still have a direct query in a rarely-used report.

The order of operations

This is the part worth memorising, and it comes from WooCommerce's guide for large stores. The migration is a hot one — no planned downtime, no maintenance window — but only if you do it in this sequence.

  1. Turn on synchronisation while leaving the posts table authoritative. Both stores now hold the data; the old one is still the source of truth. Nothing user-facing has changed, and you can stop here indefinitely.
  2. Copy the history across: wp wc hpos sync. Left to the built-in scheduler this happens in the background in batches; the CLI just does it as fast as your database will go. Verify with wp wc hpos verify_cot_data --verbose.
  3. Now switch HPOS to authoritative, in the same settings screen. This is the actual cutover, and by this point it is a pointer change — the data has been sitting in the new tables for however long you left step 2 running.
  4. Disable sync-on-read with add_filter( 'woocommerce_hpos_enable_sync_on_read', '__return_false' );
  5. Watch it for a while, then turn compatibility mode off entirely. The WooCommerce guide waited six hours before this step. On a store where orders carry real money I'd want a full business day, including whatever nightly jobs you run.

The failure mode people hit is switching authority first and syncing second, on the reasonable-sounding theory that you flip the switch and let it catch up. Don't. In that order there's a window where the authoritative store is the one that doesn't have your orders in it yet.

How long the copy takes

Longer than you think, and this is the single most useful number in the official guide: their test against a store with nine million orders took about a week.

Most stores are nowhere near nine million and will finish in minutes or hours. But the shape of that number is the point — this scales with order history, it is not instant, and it is not something to start on a Friday afternoon and watch. Run it on a staging copy of the real production database first, specifically so you find out what your number is before it matters. A clean test install will tell you nothing, because the whole cost is the history it doesn't have.

Roll back, and what it costs

Reverting is supported — you switch authority back to the posts table — and it is also a hot migration. The catch is that it isn't instantaneous either: if you've been running on HPOS with sync off, the posts table is now stale, and going back means waiting for the backfill to run before that store is trustworthy again.

Which reframes step 5 above. Compatibility mode costs you some write throughput, since every order write goes to two places. That is the price of a fast rollback, and for the first few days after cutover it is worth paying. Turning it off is a decision to give that up, so make it deliberately rather than as cleanup.

What actually improves

Set expectations correctly, because the wrong ones make this feel like a disappointment. HPOS makes order queries faster: the admin order list, search and filtering, reports, anything that scans order history. On a store with a long history that's the difference between an order screen that responds and one you avoid opening.

It does not make your storefront faster. Product pages, category listings and the cart don't touch order storage. If your problem is a slow catalogue, this is the wrong fix and you want the page cache, the object cache and your autoloaded options — which I've written about separately in what breaks first when a WooCommerce store gets busy.

Do it on a staging clone of production data. Audit the plugins that touch orders. Sync before you switch. That's the whole thing, and done in that order it's an afternoon of waiting rather than a story you tell later.