If your Orders screen is full of Failed rows, you're not doing anything wrong and neither is Woo. A failed order is a normal side effect of how online payments work. But left alone they stack up, bury the orders you actually need to act on, and make the whole store feel broken. Here's why they exist, how to quiet the noise, and a snippet that clears out the dead ones on a schedule.
It looks backwards the first time you notice it. A shopper reaches checkout, their card is declined, and yet WooCommerce has already created an order and marked it Failed. Why make an order for a payment that never happened?
Because the payment gateway needs something to attach the charge to. When a customer hits Place Order, Woo creates the order first so it has a real order ID and total, then hands that off to Stripe, PayPal, or whichever gateway you use. The gateway attempts the charge against that order. If the card is declined or the customer abandons the popup, the attempt comes back as a failure and Woo flips the order to Failed. Because the order had to exist before the charge could be tried, a failure always leaves a record behind.
This is by design, and it is genuinely useful. Because the order already exists, the customer can come back and pay for the exact same order instead of starting over. A retry through the order-pay link reuses the same order rather than spawning a new one, which is what makes payment recovery possible. So a Failed order is not garbage by default. It is a sale that has not happened yet. The trick is telling the recoverable ones from the truly dead ones, and not letting the dead ones swamp your dashboard.
The first thing that makes failed orders feel overwhelming is not the orders themselves, it is the email for every single one. By default WooCommerce sends you a notification each time an order fails. On a busy store, or during a run of declines, that is a stream of near-identical emails that trains you to ignore the inbox entirely.
You can switch them off without losing the order records. Go to WooCommerce → Settings → Emails → Failed order and disable it. While you are there, do the same for Cancelled order, which is the other one that tends to fire a lot without telling you anything you need to act on in the moment. The orders still appear on your Orders screen either way. You are only turning off the per-event email, not the record.
One honest caveat: turning these off means you are no longer pinged the instant a single payment fails. If catching individual failures fast matters to you, keep the email on but route it somewhere it stands out, or use an outside watcher instead.
A lot of duplicate failed orders come from customers trying again the wrong way. Their card is declined, they close the tab, then later they start a brand new checkout from scratch. Now you have two failed orders for one person who only ever wanted to buy once.
WooCommerce already has the right path built in. Every failed order has a Pay for this order link. A logged-in customer can find it under My Account → Orders, where a failed order shows a Pay button that takes them back to the same order to try a different card. You can also resend that link yourself: open the order in the admin, and the customer-facing pay URL is available from the order screen so you can paste it into a reply.
Guiding customers to that link instead of a fresh checkout means their second attempt reuses the original order. When it succeeds, the same order simply moves to Processing. No duplicate, no second Failed row, and the recovery lands right on the order that failed.
After the noise is down and customers have a clean way to retry, you are left with the genuinely dead failures: orders that failed and were never paid, and never will be. Those are the ones worth clearing. The snippet below runs once a day, finds failed orders older than 72 hours that were never paid, and moves them to the trash.
Why the 72 hour delay, and not instant? Because a failed order is not always final. When a customer comes back and their retry succeeds, WooCommerce flips that same failed order to Processing. If you delete failures the moment they happen, you destroy orders that were about to be recovered and break the payment-recovery flow entirely. A 72 hour window leaves plenty of room for a customer to come back and pay before the order is treated as dead. If someone has not returned in three days, it is almost certainly gone.
<?php
/**
* Move stale unpaid failed orders to trash once a day.
* Change the window with the dsg_failed_order_cleanup_hours filter.
*/
// Register the daily job on init, guarded so it schedules only once.
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'dsg_failed_order_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'dsg_failed_order_cleanup' );
}
} );
// The job itself: trash failed orders older than the window.
add_action( 'dsg_failed_order_cleanup', function () {
if ( ! function_exists( 'wc_get_orders' ) ) {
return;
}
// Default window is 72 hours. Override it with the filter below.
$hours = (int) apply_filters( 'dsg_failed_order_cleanup_hours', 72 );
$cutoff = time() - ( $hours * HOUR_IN_SECONDS );
$order_ids = wc_get_orders( array(
'status' => 'failed',
'date_created' => '<' . $cutoff,
'limit' => -1,
'return' => 'ids',
) );
foreach ( $order_ids as $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
continue;
}
// false means send to trash, not hard-delete, so it is recoverable.
$order->delete( false );
}
} );
A few notes on how it works. The init hook registers the daily event, and wp_next_scheduled makes sure it is only scheduled once no matter how many times the page loads. The wc_get_orders query asks only for failed orders created before the cutoff, and returns IDs so it stays light even if there are a lot of them. The important line is $order->delete( false ): passing false sends the order to trash rather than deleting it forever, so anything caught by mistake can be restored. And the dsg_failed_order_cleanup_hours filter lets you widen or narrow the window without touching the rest of the code.
If you would rather not touch code at all, we packaged this exact behavior (the email muting plus the scheduled cleanup with the recovery window) as a tiny free plugin: download Quiet Failed Orders, then upload it under Plugins, Add New, Upload Plugin. Settings live under WooCommerce, Settings, Advanced. It makes no external requests and orders only ever go to the trash, never deleted outright.
Drop the snippet in your child theme's functions.php, or paste it into a code snippets plugin if you would rather not edit theme files. A snippets plugin is the safer choice for most people, because the code survives theme updates and is easy to switch off if you ever want to.
To confirm it ran, wait a day and look under WooCommerce → Orders → Trash. If you had old unpaid failures, they should be sitting there. Trash is the proof it fired, and it is also your safety net: nothing is gone for good until Trash is emptied, so you can restore anything that should not have been swept up. If you want to see it act sooner, temporarily set the filter to a small number of hours, let the daily job run, then set it back to 72.
A sudden flood of failed orders is not always ordinary declines. Sometimes it is card testing, where bots run stolen card numbers through your checkout to see which ones work, and each attempt leaves a failed order behind. It looks similar on the Orders screen, but it is a different problem, and cleaning up the mess does nothing about the cause. If you see a burst of failures with odd amounts, unfamiliar names, or many attempts in a short span, read the failed-payment alerts guide for how to spot it and what to do. The cleanup snippet is for tidiness, not for defense.
Want to know your checkout is loading right now? Run the free checkout checker, no signup. And if you'd like failed payments and stopped orders to reach you within minutes without any setup on your side, Store Guardian is free while it's in beta.
WooCommerce orders suddenly stopped? Work through these 7 checks
WooCommerce checkout not working: find the cause in 15 minutes
How to get alerted the minute a WooCommerce payment fails