If your WooCommerce Orders screen suddenly fills with hundreds or thousands of Failed orders for the same cheap product, your checkout is being used for card testing. Bots run stolen card numbers through your payment gateway to see which ones still work.
This came up again on the WordPress.org forums this week: a store owner with more than 8,000 failed attempts, a security plugin that stopped helping, and an admin screen that only lets them delete 20 orders at a time. The fix has two halves. First make every checkout attempt expensive for the bot, then clean up the mess from the command line.
WooCommerce’s own card testing guide names the same symptom: a large increase in orders with the Failed status. If that is what you see, start with Step 1 right away.
Step 1. Turn on checkout rate limiting in WooCommerce
WooCommerce ships rate limiting for the Checkout block and the Store API, but it is off by default. Go to WooCommerce > Settings > Advanced > Features and enable Rate limiting Checkout block and Store API.
According to the Store API rate limiting docs, this switch only covers the POST /checkout and Place Order flow of the Checkout block, with a limit of 3 requests per 60 seconds. A real customer never needs more than that.
The rest of the Store API (cart and add-to-cart requests) is only rate limited when you enable it with the woocommerce_store_api_rate_limit_options filter. I put it in a small must-use plugin so a theme switch can’t remove it. Create wp-content/mu-plugins/store-api-rate-limit.php:
<?php
// Rate limit Store API POST requests (cart, checkout).
add_filter( 'woocommerce_store_api_rate_limit_options', function () {
return array(
'enabled' => true,
'proxy_support' => false, // true only if the site sits behind Cloudflare or another proxy
'limit' => 25, // requests per timeframe (WooCommerce default)
'seconds' => 10, // timeframe in seconds (WooCommerce default)
);
} );
Only POST requests are counted, so normal browsing is not affected. I keep the default 25 requests per 10 seconds here because add-to-cart clicks count too, and a tight limit would hurt real shoppers.
💡 If the site is behind Cloudflare, set proxy_support to true. Otherwise every visitor looks like the same proxy IP and real customers get blocked together with the bots.
Step 2. Put a challenge in front of the checkout
Rate limiting slows a bot down. A challenge stops most of them. WooCommerce recommends a CAPTCHA, and the free Simple Cloudflare Turnstile plugin adds Cloudflare Turnstile to the WooCommerce Checkout and Pay For Order forms without any code. Its changelog also lists support for the Checkout block and the Store API.
Install it, create a Turnstile widget in your Cloudflare dashboard, paste the site key and secret key into the plugin settings, and tick the WooCommerce checkout options. Then place a test order yourself to make sure the widget shows and the order goes through.
If you use Cloudflare, you can also add a rate limiting rule at the edge. The Cloudflare docs list one rate limiting rule on the Free plan, counted per IP over 10 seconds, with the request path as the match field. Point it at the Store API checkout path:
(http.request.uri.path eq "/wp-json/wc/store/v1/checkout")
Pick a low request count and the Block action. Legit shoppers hit that endpoint once per order, so even a limit of a few requests per 10 seconds leaves them alone.
Step 3. Tighten the store while the attack lasts
These come straight from the WooCommerce guide and cost you little for a few days:
- Disable guest checkout in WooCommerce > Settings > Accounts & Privacy, so a bot has to create an account first.
- Temporarily make the cheapest products private. Testers love low-price items and pay-what-you-want or donation products with no minimum.
- Ask your payment provider to tighten security on your account.
- Review the successful transactions from the attack window and refund any that look fraudulent, before the chargebacks arrive.
Step 4. Delete thousands of failed orders with WP-CLI
Only clean up after the attack is blocked, otherwise new failed orders keep coming in. And take a database backup first, because deleted orders are gone for good:
wp db export before-failed-orders-cleanup.sql
You should see a Success: Exported to 'before-failed-orders-cleanup.sql'. line. Now count the failed orders created since the attack started (change the date):
wp eval 'echo count( wc_get_orders( array( "status" => "failed", "date_created" => ">2026-11-01", "limit" => -1, "return" => "ids" ) ) ), PHP_EOL;'
The number should roughly match what the Failed tab shows. If it is far off, check the date before you delete anything.
Next, save this as delete-failed-orders.php outside your web root. It deletes 500 orders per run through wc_get_orders() and $order->delete( true ), so it works with both HPOS and the old posts table storage, and the order items and notes go with it:
<?php
$ids = wc_get_orders( array(
'status' => 'failed',
'date_created' => '>2026-11-01',
'limit' => 500,
'return' => 'ids',
) );
foreach ( $ids as $id ) {
$order = wc_get_order( $id );
if ( $order ) {
$order->delete( true ); // true = skip the trash
}
}
echo count( $ids );
Run it in a loop until nothing is left:
while :; do
n=$(wp eval-file /path/to/delete-failed-orders.php) || break
echo "Deleted: $n"
[ "$n" = "0" ] && break
done
Each pass prints how many orders it removed, and the loop stops at Deleted: 0 or on the first WP-CLI error. Batches keep memory use low, the same approach I used for bulk deleting WooCommerce products with WP-CLI.
💡 Some failed orders may be real customers whose card was declined. If you have a few legitimate ones, narrow the date range or note their order IDs before you run the loop.
Deleting orders can leave background jobs behind. If the Action Scheduler queue piles up afterwards, clear the past-due actions with WP-CLI.
Why blocking IPs alone does not stop it
Card testers rotate through proxies, so blocking IPs one by one is a game you lose. What works is raising the cost of each attempt: a limit on how often anyone can hit checkout, a challenge a script can’t pass cheaply, and gateway rules that decline suspicious cards.
With all three in place, your store stops being a useful tool and the bots move on to an easier target.
On OpenPanel, the IP Blocker page takes IPs or CIDR ranges one per line, which helps when one subnet is clearly hammering checkout, and works like the IP Blocker in cPanel. WP-CLI is preinstalled in every OpenPanel PHP container, so the cleanup loop above runs from the web terminal or a cron job. Treat both as backups to Steps 1 and 2, not a replacement.
If Cloudflare sits in front of the store, keep the admin side out of the way of your new rules. I covered that in how to exclude wp-admin from Cloudflare.
TL;DR: A flood of Failed orders means card testing. Enable Rate limiting Checkout block and Store API under WooCommerce > Settings > Advanced > Features, add Turnstile to checkout, disable guest checkout for a few days, then back up the database and delete the failed orders in batches with wc_get_orders() and WP-CLI. These are the options WooCommerce and Cloudflare document as of 2026-11-13, so recheck the setting names if your versions are newer.

