WooCommerce Failed Orders: The Early Warning Nobody Is Watching.

Orders moved to a new table and the old query returns ten drafts instead. Then the failed-to-total ratio is the signal worth alerting on.

Aditya Sharma·10 min read

Short answer: a rising rate of woocommerce failed orders means payments are being attempted and refused, which is a gateway problem the store cannot see. Every URL returns 200 throughout. And before you can monitor it, you need to be querying the right table, because orders moved, though not on every store, and not always by default.

Orders are not in wp_posts any more

I created ten orders on a current WooCommerce install and counted them both ways, then went back and tested the migration mechanics directly on that same install to confirm exactly when each table actually applies rather than assume it from the version number alone.

Two SQL queries compared. The legacy query against wp_posts with post_type shop_order returns ten rows all with status draft. The query against wp_wc_orders returns the real distribution: three completed, two failed, and one each of on-hold, cancelled, processing, pending and refunded
The legacy query returns ten rows, all draft, and none of them are orders. Any

monitoring built on wp_posts reports a store with no sales while it trades normally.*

That single option check is the whole difference. This is the same shape as the autoload column that no longer contains the value every tutorial queries for: the query still runs, still returns rows, and describes nothing.

If you have a dashboard, a report or an alert that reads orders from wp_posts, check what it is actually returning. A silent zero is indistinguishable from a healthy quiet period.

Checking whether HPOS is even on before you write the query

The wp_wc_orders query above assumes the new order storage, HPOS, is active. I checked a fresh WooCommerce 11.1.0 install to find out whether that is now the default, since assuming it is the wrong direction to get this backwards.

wp option get woocommerce_custom_orders_table_enabled
no

Not enabled, on a current WooCommerce version, installed clean with no history to migrate. HPOS is opt-in, toggled from Settings → Advanced → Features, not something a store arrives with switched on by default. That matters for this post’s own premise: the trap is not “your store has moved and your query has not caught up.” It is narrower and depends entirely on which of two states a specific store is actually in, and the only way to know is to check, not assume either direction.

I created two real test orders on this legacy-storage install and queried both tables to see exactly what each one returns:

wp db query "SELECT post_status, COUNT(*) FROM wp_posts WHERE post_type='shop_order' GROUP BY post_status;"
wp db query "SELECT status, COUNT(*) FROM wp_wc_orders GROUP BY status;"
post_status   COUNT(*)
wc-completed  2
wc-failed     1

ERROR 1146 (42S02): Table 'wp.wp_wc_orders' doesn't exist

On a legacy-storage store, wp_posts returns the real, correct order statuses, exactly as it always has, and the wp_wc_orders query from earlier in this post does not return an empty or misleading result. It fails outright with a table-not-found error. That is actually the safer of the two possible mistakes: a hard SQL error gets noticed immediately, unlike the reverse case this post opened with, where a legacy query against a migrated store returns real rows that quietly mean nothing. Check woocommerce_custom_orders_table_enabled before writing either query, and let that answer decide which table you query, rather than assuming one direction based on how recent the WooCommerce version is.

Which statuses matter, and where WooCommerce failed orders sit

Table of WooCommerce order statuses with whether money was taken and what each means for monitoring. Completed and processing are healthy. Failed means payment was attempted and refused. Pending means created but never paid. On-hold, cancelled and refunded are usually deliberate
failed and pending are the two worth alerting on. A declined card produces one failed

order. A gateway that stopped authenticating produces a run of them.*

StatusMoney taken?What it means for monitoring
completedyeshealthy, the baseline you compare against
processingyespaid, awaiting fulfilment, healthy
failednopayment attempted and refused; rising rate is a gateway problem
pendingnocreated, never paid; a spike means checkout is breaking mid-flow
on-holdawaitingexpected for bank transfer, unexpected elsewhere
cancellednousually deliberate
refundedreverseda business signal rather than a technical one

The distinction matters because failed is not a customer problem. A customer with an expired card produces one failed order and then usually pays with another. A store producing a run of failed orders with no successes between them has an infrastructure problem, and nobody is being told.

pending deserves attention too. An order created and never paid means the customer reached the final step and something stopped them, which is the most expensive point at which to lose somebody.

Alerting without crying wolf

Table of five alerting approaches. A fixed order-count threshold is not useful. Comparing against the same hour last week, alerting on the failed-to-total ratio, and alerting on consecutive failures all work. Alerting on zero orders needs care
The ratio check is the one I would build first. It needs no history, no baseline and no

tuning.*

ApproachUseful?Why
alert if orders today < a fixed numbernofires every Sunday on a B2B store, never on a quiet Tuesday
compare against the same hour last weekyeshandles daily and weekly shape with no configuration
alert on the failed-to-total ratioyesindependent of volume, works on a quiet store
alert on a run of consecutive failuresyesthree failed and no successes is a gateway, not coincidence
alert on zero orders in N hourswith careonly useful once you know the genuine quiet period

A fixed threshold fails immediately, because a store’s normal varies by hour and weekday. On a B2B store, Sunday is quiet by design and an alert that fires every Sunday gets muted within a fortnight.

The failed-to-total ratio sidesteps all of that. It is independent of volume, so it works on a store taking three orders a day and one taking three hundred.

WooCommerce itself refuses to make this worse

While testing the HPOS toggle on the lab install, I tried flipping the storage setting directly to see how a mid-migration store behaves, and WooCommerce stopped me:

wp option update woocommerce_custom_orders_table_enabled yes
Fatal error: Uncaught Exception: The authoritative table for orders storage can't be
changed while there are orders out of sync

That is a real guard, not a bug I stumbled into. WooCommerce checks whether every order is already synchronised between wp_posts and wp_wc_orders before it will let a site switch which table is authoritative, and refuses the switch outright if any are not. The exact trap this post opens with, a store where the two tables disagree about what an order’s status is, is one WooCommerce’s own core code actively tries to prevent from being the operating state for very long. Reads and writes through WooCommerce’s own APIs, the order object, the REST endpoints, the admin screens, stay correct throughout a migration because WooCommerce itself enforces synchronisation before it will call the switch complete.

The actual danger this post is about lives specifically in code that bypasses those APIs: a direct SQL query against wp_posts, written when that was the only table that existed and never revisited, run against a store that has since finished a migration WooCommerce itself guarded carefully. WooCommerce protects its own consistency. It cannot protect a dashboard query, a custom report, or an old cron job that never got told the table changed underneath it, which is exactly why the check this post recommends, confirm which table is authoritative before trusting either query, is doing work WooCommerce’s own safeguards cannot do for you.

The query

SELECT status, COUNT(*) AS n
FROM wp_wc_orders
WHERE date_created_gmt > DATE_SUB(UTC_TIMESTAMP(), INTERVAL 24 HOUR)
GROUP BY status;

Compare failed against completed plus processing. On most stores the failed share sits in low single digits. A jump to a third, or any period with failures and no successes at all, is worth a phone call.

Two caveats worth knowing. Sites that have not migrated to the new order storage still use wp_posts, so check which applies before writing anything. And a store with no orders in the window returns no rows rather than zeros, which is easy to misread as healthy.

Why this is the check worth having

Order-rate monitoring is the only store check that measures the outcome rather than a proxy.

Uptime tells you the server answered. A checkout probe tells you the page rendered. Neither tells you whether money moved. A gateway can accept the page load, present the payment form, and refuse every transaction, and every technical check will pass.

That is the same category of silent failure as a contact form that reports success and delivers nothing, with a price attached.

Protuno’s eCommerce agent, Till, carries a failed-order-rate check against the store’s own baseline alongside the checkout probe. Straight with you as on every post here: Till is built and named but not live yet.

Run the query on your busiest client’s store. If it returns nothing because you were querying wp_posts, that is today’s finding.

Building the ratio check so it survives a future migration

The safest version of the monitoring query this post recommends is one that does not assume which table is authoritative, because that answer can change under a store you are watching without anyone telling you first. A migration a client runs themselves, a hosting provider that bundles a WooCommerce update with HPOS enabled by default in some future release, or an agency switching it on deliberately during a maintenance window, all change which table holds the truth without necessarily changing anything else about how the site behaves day to day.

A monitoring script that hardcodes wp_wc_orders will fail loudly, the same table-not-found error this post’s test produced, the day it runs against a store that has not migrated. One that hardcodes wp_posts will fail quietly, returning real-looking rows describing nothing, against a store that has. Neither failure mode is acceptable in something meant to run unattended on a schedule, so the check worth writing first is not the order-status query. It is the one-line lookup that decides which query to run:

wp option get woocommerce_custom_orders_table_enabled

yes routes to wp_wc_orders. no, or the option missing entirely on an older WooCommerce install that predates the setting, routes to wp_posts. That single check, run before the actual monitoring query on every execution rather than assumed once and hardcoded, is what keeps a failed-order alert working correctly through a migration it was never told was happening, which is exactly the kind of silent underlying change this entire post is about.

What this means for a query written before HPOS existed

The practical audit worth running today, on every client store with WooCommerce-based monitoring already in place: find every place a report, a dashboard widget, a cron job or a custom SQL query reads order data, and check whether any of them hardcode a table name rather than checking the authoritative-storage option first.

Most of that code was written before HPOS existed, because HPOS is a relatively recent addition to WooCommerce and wp_posts was the only option for years before it. None of that code is wrong for the store it was written against. It becomes wrong silently, the moment that specific store’s storage setting changes, and nothing about the code itself will tell you that happened. The option lookup this post recommends costs one extra query and turns a silent, indefinite failure into a check that keeps working correctly regardless of which side of a migration the store you are monitoring happens to be on today, or moves to next quarter without telling anyone who built the dashboard.

Comments