Why Your WooCommerce Webhooks Stopped Working — And Nobody Told You

· 12 min read

Two Weeks of Missing Orders

A UK ecommerce client rang me on a Monday morning with a problem that had apparently been brewing for a fortnight. Their WooCommerce store processes around 80 orders per day, and every order is supposed to fire a webhook to their third-party logistics provider. The 3PL receives the webhook, creates a pick-and-pack job, and ships the order.

For the last two weeks, the 3PL hadn't received a single webhook. The warehouse team had been manually copying orders from the WooCommerce dashboard into their system — nobody had escalated it because they assumed "the developer was working on it." The developer (me) had no idea anything was wrong.

When I logged in and navigated to WooCommerce > Settings > Advanced > Webhooks, the status said one word: Disabled.

Nobody had disabled it. WooCommerce had done it silently.

The 5-Strike Rule Nobody Reads About

WooCommerce has a built-in safety mechanism that most store owners never learn about until it bites them. If a webhook accumulates 5 consecutive delivery failures — meaning the endpoint returns anything other than a 2xx, 301, or 302 response — WooCommerce disables it on the next failed attempt (the 6th delivery). The code in class-wc-webhook.php checks the existing failure_count before incrementing, so the threshold comparison happens against the count from prior deliveries.

There is no email notification. No admin notice. No dashboard warning. The webhook just stops firing, and the failure counter resets to zero so there's no visible trace of what happened.

The logic lives in the deliver() method. A successful delivery resets the failure counter to zero. Anything else increments it. Once the counter meets the threshold (5 by default), the webhook's status flips to disabled and an action hook fires — but unless you've written custom code listening for woocommerce_webhook_disabled_due_delivery_failures, nothing happens with that signal.

Finding Out What Went Wrong

The first thing I checked was the WooCommerce webhook delivery log. Navigate to WooCommerce > Status > Logs and filter by the webhooks-delivery source. WooCommerce core always writes to this log for every delivery attempt. However, the log only includes full request and response bodies when WP_DEBUG is enabled — without it you'll see delivery outcomes but not the detailed payloads:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

With logging enabled, each delivery attempt records the URL, the HTTP response code, the response body, and the duration. On this client's site, the last five log entries before the webhook went silent all showed the same thing:

Webhook delivery failed: HTTP 503 - URL: https://api.their3pl.com/inbound/orders
Response body: Service Temporarily Unavailable
Duration: 30.02s

The 3PL's API had gone down for maintenance for roughly 45 minutes, two weeks earlier. During that window, five orders came through the store, each triggering a webhook delivery attempt, each getting a 503 back. Five strikes. Webhook disabled. Every order after that — over 1,100 of them — never reached the warehouse.

Why Action Scheduler Makes This Worse

Since WooCommerce 3.5, webhooks are delivered asynchronously through the Action Scheduler. When an order is placed, WooCommerce doesn't fire the webhook inline during the checkout request. Instead, it schedules an action that runs in the background.

This is generally a good design — it keeps checkout fast and avoids timeouts. But it means webhook deliveries depend on the Action Scheduler actually running. The Action Scheduler relies on WP-Cron, which is triggered by site visits. On a store with steady traffic, this works fine. On a store with quiet periods — overnight, weekends, holiday closures — scheduled actions can pile up and deliver in bursts.

The problem for failure counting is that burst delivery amplifies the damage. If the receiving endpoint is down for 10 minutes and 12 orders come in during that window, WooCommerce doesn't spread those delivery attempts over time. The Action Scheduler fires them as fast as it can, often within seconds of each other. Five failures in rapid succession, webhook disabled, and the remaining seven orders never even get attempted.

You can check for stuck or failed webhook actions in the Action Scheduler:

wp action-scheduler list --group=woocommerce --status=failed --per-page=20 --user=1

Or via the admin at WooCommerce > Status > Scheduled Actions, filtering by the failed tab.

The Fix: More Than Just Re-Enabling

The immediate fix is straightforward — go to WooCommerce > Settings > Advanced > Webhooks, edit the webhook, change the status back to Active, and save. But that only solves the symptom. Here's what I actually did for this client.

1. Increase the Failure Threshold

Five consecutive failures is aggressive. A single brief endpoint outage during a busy period can hit that limit in under a minute. I added this to the theme's functions.php (or better, a site-specific plugin):

add_filter( 'woocommerce_max_webhook_delivery_failures', function () {
    return 25;
} );

Twenty-five gives enough headroom to survive a 30-minute endpoint outage without disabling the webhook, while still protecting against a permanently dead URL accumulating infinite failed requests.

2. Add Alerting When Failures Occur

WooCommerce fires the woocommerce_webhook_delivery action after every delivery attempt, successful or not. I added a lightweight monitor that sends an email after the third consecutive failure — enough to alert before the webhook gets disabled:

add_action( 'woocommerce_webhook_delivery', function ( $http_args, $response, $duration, $arg, $webhook_id ) {
    $webhook = wc_get_webhook( $webhook_id );
    if ( ! $webhook ) {
        return;
    }

    $response_code = wp_remote_retrieve_response_code( $response );
    if ( $response_code >= 200 && $response_code < 300 ) {
        return;
    }

    $failure_count = $webhook->get_failure_count();
    $max_failures  = apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 );
    $alert_at      = max( 3, intval( $max_failures * 0.5 ) );

    if ( $failure_count === $alert_at ) {
        wp_mail(
            get_option( 'admin_email' ),
            'WooCommerce Webhook Delivery Failures',
            sprintf(
                "Webhook #%d (%s) has failed %d consecutive deliveries.\nEndpoint: %s\nLast response: HTTP %d\n\nIt will be disabled after %d failures.",
                $webhook->get_id(),
                $webhook->get_name(),
                $failure_count,
                $webhook->get_delivery_url(),
                $response_code,
                $max_failures
            )
        );
    }
}, 10, 5 );

3. Switch to System Cron

If the store relies on webhooks for critical workflows — fulfillment, accounting, CRM sync — WP-Cron isn't reliable enough. I've written before about why WP-Cron fails on WooCommerce stores and this is another case where it matters. I disabled WP-Cron and replaced it with a system cron job:

// wp-config.php
define( 'DISABLE_WP_CRON', true );
# /etc/cron.d/woocommerce-cron
* * * * * www-data cd /var/www/html && wp cron event run --due-now --user=1 --quiet 2>&1 | logger -t wp-cron

Running cron every minute ensures the Action Scheduler processes webhook deliveries promptly instead of waiting for the next visitor.

4. Monitor Webhook Status with WP-CLI

For ongoing maintenance, I added a health check script that runs daily and alerts if any webhook is in a disabled state:

#!/bin/bash
DISABLED=$(wp wc webhook list --status=disabled --user=1 --format=count 2>/dev/null)

if [ "$DISABLED" -gt 0 ]; then
    wp wc webhook list --status=disabled --user=1 --fields=id,name,status,delivery_url --format=table
    echo "WARNING: $DISABLED disabled webhook(s) found"
    # Send alert via your preferred method
fi

Add this to your server monitoring or run it as a cron job. It catches disabled webhooks before your warehouse team starts manually copying orders.

Replaying the Missed Orders

With 1,100 orders that never reached the 3PL, I needed to replay them. WooCommerce doesn't have a built-in webhook replay feature, but you can trigger a re-delivery by updating the order's status. For this client, I wrote a quick WP-CLI command:

wp wc webhook list --status=active --user=1 --format=ids

This confirmed the webhook was active again. Then I identified the affected orders — everything between the date the webhook was disabled and today — and triggered a re-delivery by touching each order:

wp db query "
  SELECT ID FROM wp_posts
  WHERE post_type = 'shop_order'
    AND post_status = 'wc-processing'
    AND post_date >= '2026-04-11'
  ORDER BY post_date ASC
" --skip-column-names | while read order_id; do
    wp wc order update "$order_id" --status=processing --user=1 --quiet
    sleep 0.5
done

The half-second sleep prevents hammering the 3PL endpoint. Each status update triggers the order.updated webhook, which sends the full order payload to the endpoint. The 3PL's system was idempotent — duplicate orders were detected and ignored — so there was no risk of double-shipping.

For stores using HPOS (High-Performance Order Storage), adjust the query to use the wp_wc_orders table instead:

wp db query "
  SELECT id FROM wp_wc_orders
  WHERE type = 'shop_order'
    AND status = 'wc-processing'
    AND date_created_gmt >= '2026-04-11'
  ORDER BY date_created_gmt ASC
" --skip-column-names | while read order_id; do
    wp wc order update "$order_id" --status=processing --user=1 --quiet
    sleep 0.5
done

What Your Webhook Endpoint Should Do

The other side of this equation is the receiving endpoint. If your 3PL, CRM, or accounting integration is the one returning errors, there are a few basics that prevent cascading failures:

  • Return 200 immediately. Accept the payload, queue it for processing, and return a 200 before doing any heavy work. If your endpoint takes 30 seconds to process an order before responding, WooCommerce may time out and count it as a failure.
  • Handle duplicates. Use the order ID or a webhook delivery ID as an idempotency key. You will receive duplicate deliveries, especially after re-enabling a webhook or during retries.
  • Monitor your own uptime. If your endpoint goes down, you have less than five webhook deliveries before WooCommerce cuts you off. Set up health checks on your receiving infrastructure independently.

Preventing This in the Future

The core issue is that WooCommerce treats webhook reliability as the store owner's problem while providing almost no visibility into failures. Until WooCommerce adds proper webhook observability — delivery dashboards, failure alerting, manual retry buttons — you need to build that layer yourself.

The minimum I now set up on every WooCommerce store with webhook integrations:

  1. Increase failure threshold to 25
  2. Add email alerting at 50% of the threshold
  3. System cron instead of WP-Cron
  4. Daily WP-CLI health check for disabled webhooks
  5. Uptime monitoring on both the store and the receiving endpoint

It takes about 20 minutes to set up and saves the kind of two-week silent outage that cost this client staff time, customer trust, and a very stressful Monday morning. If your WooCommerce store depends on integrations that can't afford downtime, this is exactly the kind of thing a proper maintenance plan catches before it becomes a crisis.


Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans → Get in Touch →

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs