Checkout Bouncer

Notes

What a wall of failed WooCommerce orders is really telling you

Failed orders are declines, not abandoned baskets. And the row count is not the attempt count, because WooCommerce reuses an order when the basket has not changed.

6 min read

You open the orders screen and the top of the list is red. Ten failed orders, then twenty, most of them inside the same half hour, none of them yours. The instinct is to reach for a blocking tool. The better first move is to read what the list is actually saying. WooCommerce failed orders arrive in bursts like this for two very different reasons.

WooCommerce records more about each attempt than the status column shows, and it also conceals something important. A count of failed orders is not a count of payment attempts. Both halves change what you do next.

What WooCommerce failed orders actually are

WooCommerce's own documentation defines the statuses precisely, and the distinctions repay a slow read. Failed is "The customer's payment failed or was declined, and no payment has been successfully made". Pending payment is "The order has been received, but no payment has been made", and those orders are "generally awaiting customer action". On hold is "The order is awaiting payment confirmation". Cancelled is "The order was canceled by an admin or the customer".

Draft is the one people have not met yet: "Draft orders are created when customers start the checkout process while the block-based checkout is in place". If your store uses the Checkout block, a row can exist before anybody has tried to pay at all.

So a screen of WooCommerce failed orders is a wall of declines, not a wall of abandoned baskets. That is the first thing worth knowing about them. Abandonment settles in pending or draft. A decline needs a card, a gateway and an answer from a bank. Something reached your payment processor and it refused. Each of those attempts is a request you paid to make in one currency or another.

Why the count is not the number of attempts

Before the classic checkout inserts an order, it looks for one it can reuse. It reads the order ID kept in the session under order_awaiting_payment and compares a hash of the current cart against that order. If the order still has the pending or failed status and the basket has not changed, it resumes that one. The source is direct about the rule: if the order has changed, meaning different items or cost, create a new order.

Now read that as a script rather than a shopper. A run that holds one session and one basket, changing only the card details between submissions, can push many declines through a single order record. A run that opens a fresh session for every card leaves a separate row each time. Identical volumes of abuse, wildly different order lists.

Which means a list of WooCommerce failed orders is a shape, not a tally. If you want the number of attempts, count them where the record actually lives. Your payment provider's dashboard holds them. So does the gateway log under WooCommerce, Status, Logs, for gateways that write one once you switch logging on in their settings.

What each row already holds

Open one of the WooCommerce failed orders. WooCommerce stamps every order it creates with the route that made it. The classic form gets set_created_via( 'checkout' ), and orders that begin life in the block checkout get set_created_via( 'store-api' ). If your failures all carry one value and your genuine orders carry the other, you know which door the traffic uses before you look at anything else.

The same code stores the shopper's address and browser, through set_customer_ip_address() and set_customer_user_agent(). Those two fields are the closest thing you have to a fingerprint. Treat the address with care if your site sits behind Cloudflare, a load balancer or any other reverse proxy. What lands in that field is not always the visitor. That is a subject of its own, and it is the next note in this series.

Then read the order notes. A gateway that declines a payment usually writes the reason into the order as a note, and the wording matters. A bank saying the card has expired is a different world from a bank saying the card is stolen. Different again is the same processor returning one generic decline forty times in a row.

Reading the shape

Ordinary decline traffic and card testing look nothing alike once you stop counting and start sorting. The evidence that separates them is worth reading alongside this. Ordinary declines are spread across the day, across different baskets, and across customers who often come back and succeed on the second try. Automated abuse tends to arrive as a burst, with the cheapest thing in your catalogue in the basket, and it never returns to buy anything.

Four sorts will separate them faster than any tool. Sort by time and look for compression. Sort by cart contents and look for repetition. Sort by email address and look for a pattern in the local part, or for a stream of addresses at one domain. Sort by billing address and look for the same address behind many different names.

One caution about that last one. A shared address across a burst is evidence. A single address across a whole day is often just a household, an office, or a mobile network. Blocking it costs you a real customer who will never tell you what happened.

Before you clear them out

The temptation with a screen of WooCommerce failed orders is to select all and delete, and it is worth resisting for a week. Those rows are the only record you have of the attempts, and once they are gone the question "when did this start?" has no answer.

WooCommerce will do the clearing for you on a schedule if you ask it to. Under WooCommerce, Settings, Accounts and Privacy, the personal data retention section carries a Retain failed orders option. WooCommerce describes it as "Failed orders are unpaid or abandoned and should not need to be fulfilled". WooCommerce moves cleaned-up failed, pending and cancelled orders to the trash, while it anonymises completed orders instead, so sales statistics stay accurate. Leaving a field blank retains that data indefinitely. Set it deliberately rather than discovering it later: a retention period shorter than your investigation is a retention period that deletes your evidence.

The real fix is upstream anyway. Every one of those WooCommerce failed orders is an authorisation your processor already attempted. The way to stop paying for them is to stop the submission reaching the gateway, not to tidy up after it has.

Where Checkout Bouncer fits

Checkout Bouncer is a free, GPL plugin on WordPress.org that scores checkout submissions with Google reCAPTCHA v3 before an order exists. A run of automated attempts stops at the door rather than arriving in your order list as evidence. It records the score, the verdict, the surface and the reason for each submission, which is the log this note keeps asking you to read. It is a free, GPL plugin on WordPress.org, and the features page lists every checkout surface it scores. It is deliberately narrow: WooCommerce checkout only, not login, registration or contact forms, and it is neither a firewall nor a malware scanner. Pro remains a plan rather than a product, so there is nothing to buy. The sorting above works perfectly well by hand, and it is worth doing either way.

See which doors into your checkout are standing open