DebuggingAugust 6, 20268 min read

WooCommerce Webhooks Are Not Sent When the Order Is Placed

WooCommerce queues deliveries through Action Scheduler, which runs on WP-Cron, which runs when somebody visits your site. On a quiet store that is minutes of delay. After five consecutive failures the webhook is disabled outright.

THE ORDER IS PLACED, THE WEBHOOK IS NOT SENTorder placedAction Schedulerwoocommerce_deliver_webhook_asyncwaitsfor a page viewsentWP-Cron is not a clock. it runs when somebody visits the site.on a quiet store that is minutes. on a very quiet one it is hours.more than five consecutive failures and WooCommerce disables the webhook outright
The order is placed immediately. The delivery waits for the next page view, because WP-Cron is not a clock.

Every other webhook article, including the rest of ours, starts from the same assumption: the provider sent the event promptly and something went wrong afterwards. WooCommerce breaks that assumption before the request ever leaves the server.

The event is not sent when the order is placed. It is put in a queue, and the queue is drained by WordPress cron, and WordPress cron is not a cron. It runs when somebody loads a page.

If your store is busy, the delay is a second or two and you will never notice. If your store gets a few visitors an hour, the "real-time" order notification arrives whenever the next visitor happens to show up.

TL;DR

  • Deliveries are queued as woocommerce_deliver_webhook_async actions in Action Scheduler
  • Action Scheduler is driven by WP-Cron, which fires on page loads rather than on a schedule
  • A low-traffic store therefore has slow and unpredictable webhook delivery, and nothing about it is broken
  • "WooCommerce automatically disables a webhook after more than five consecutive delivery failures"
  • A failure is any response that is not 2xx, 301, or 302
  • The real fix on self-hosted WordPress is to disable WP-Cron and drive it from a system cron

Start by checking whether it was sent at all

Before debugging your endpoint, establish which of two very different problems you have: the delivery failed, or the delivery has not happened yet.

WooCommerce → Settings → Advanced → Webhooks shows each webhook's status and its recent deliveries with the response code and body. If deliveries are listed and failing, skip to the next section. If the log is empty and the order clearly exists, the delivery is still sitting in the queue.

THE ORDER IS PLACED, THE WEBHOOK IS NOT SENTorder placedAction Schedulerwoocommerce_deliver_webhook_asyncwaitsfor a page viewsentWP-Cron is not a clock. it runs when somebody visits the site.on a quiet store that is minutes. on a very quiet one it is hours.more than five consecutive failures and WooCommerce disables the webhook outright

To see the queue itself, install WP Crontrol or check the Action Scheduler admin screen under Tools → Scheduled Actions, and filter for woocommerce_deliver_webhook_async. Pending actions with timestamps in the past are the smoking gun: they were due, nothing ran them.

Why WP-Cron behaves this way

WordPress has no background process. What it calls cron is a list of due tasks checked on incoming requests, so the clock only advances when traffic arrives. It is a reasonable design for shared hosting in 2005 and a poor one for anything time-sensitive.

Consequences for webhooks specifically:

Delivery latency tracks your traffic, not the event. A store with steady traffic delivers in seconds. A B2B store where orders come in overnight and nobody browses until morning can sit on a queued delivery for hours.

Backlogs compound. Failed actions stay in the table and are retried, and on stores that have been running a while the Action Scheduler tables grow large enough to slow down the very requests that are supposed to drain them.

Page-load-triggered work is unreliable under caching. A full-page cache or a CDN in front of WordPress means many visits never reach PHP at all, which means they never advance cron.

The fix is standard WordPress operations and it is worth doing on any store where the webhook matters:

// wp-config.php
define('DISABLE_WP_CRON', true);
# then drive it from a real scheduler, every minute
* * * * * cd /var/www/store && wp cron event run --due-now >/dev/null 2>&1

wp action-scheduler run is the equivalent for draining the Action Scheduler queue directly if you have the WP-CLI package available. Once cron runs on a real timer, delivery latency stops depending on whether anyone is browsing.

Five failures and the webhook is gone

This is the part that turns a temporary problem into a permanent one.

"WooCommerce automatically disables a webhook after more than five consecutive delivery failures." A failure is defined broadly: "any response that is not a 2xx, 301, or 302 HTTP status code. For example, 4xx or 5xx responses count as failures."

Five is a small number. A deploy that takes your endpoint down for two minutes on a store with steady order flow will burn through five deliveries without difficulty. When it happens, the webhook's status flips to disabled and stays that way. No email, no admin notice, nothing that appears in front of a human. Orders keep being created and nothing is sent, which looks exactly like a quiet sales day.

This is the same failure shape as Shopify deleting the subscription, with two differences that make it worse: the threshold is five rather than eight over four hours, and there is no warning email at all.

If you run WooCommerce in production, put a check on the webhook's status. A scheduled task that reads the webhook via the REST API and alerts if status !== 'active' costs almost nothing and is the only thing that will tell you.

Signature verification

WooCommerce sends X-WC-Webhook-Signature, a base64-encoded HMAC-SHA256 of the raw request body using the secret you configured on the webhook.

Base64 rather than hex, which is the same convention Shopify uses and the opposite of Stripe and GitHub. If you have a shared verification helper across providers, that encoding difference is a common source of "the signature never matches" with a payload that looks perfect.

import crypto from "node:crypto";

function verifyWooCommerce(raw: string, header: string | null, secret: string) {
  if (!header) return false;
  const expected = crypto.createHmac("sha256", secret).update(raw, "utf8").digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

As everywhere, hash the bytes you received rather than a re-serialized object. The per-provider comparison is in Verifying Stripe, GitHub, and Shopify Webhook Signatures.

One WooCommerce-specific gotcha: if the webhook was created without a secret, the signature is generated from the site's own key material rather than something you chose, and your verification will fail for reasons that have nothing to do with your code. Set a secret explicitly.

The rest of the checklist

Work down these when deliveries exist and are failing.

Status is active, not paused or disabled. Check this first, because the disabled state explains everything else at once.

The topic matches the event you think it does. order.created fires on creation, which for many payment gateways happens while the order is still pending. If you are waiting for payment, order.updated with a status check is usually what you want. Plenty of "webhook not firing" reports are actually "firing on a different event than expected."

The delivery URL is reachable from the server, not just from your laptop. Stores behind a corporate network or on hosts with restrictive egress rules can fail here silently.

A plugin is short-circuiting the action. WooCommerce hooks are filterable, and caching, security, or performance plugins have been known to interfere with Action Scheduler. Testing with plugins disabled is unglamorous and frequently decisive.

The webhook's API version. WooCommerce webhooks have versioned payloads, and a v1 webhook created years ago sends a different shape than v3. If your handler started failing after a WooCommerce update, check which version the webhook is pinned to.

Where AnyHook fits

Two of the problems above are structural, and neither is fixed by a better handler.

The first is the five-failure disable. Point the webhook at in.anyhook.net/you/woo and AnyHook returns 200 in under 50ms from the edge, so the consecutive-failure counter never advances regardless of what your own endpoint is doing. Retries against your endpoint run on AnyHook's schedule, not WooCommerce's, and the webhook stays active.

The second is that WooCommerce's own delivery log lives in the store's database, on the store's retention, in an admin screen. AnyHook keeps every event with full headers and body, encrypted at rest, with replay over a time range, so a deploy window is recoverable without touching WordPress.

  • Failure alerts at 1, 5, and 20 consecutive failures, which is the notification WooCommerce does not send
  • Delivery retried with exponential backoff for far longer than a single attempt
  • One AnyHook-Signature header instead of the base64 variant, if you receive from several platforms

What a relay cannot fix is the queue in front of it. If WP-Cron is not running, the event never reaches anything. Fix the cron first; the relay handles everything after that.

Takeaway

WooCommerce is the one major platform where "the webhook is late" is usually not a bug and not your endpoint. Check the Action Scheduler queue before you check anything else, and if the store's traffic is low, move cron onto a real scheduler.

Then set a secret, verify against raw bytes with base64, and put a monitor on the webhook's own status. Five consecutive failures is not a lot of room, and the disable arrives without a word.

All postsAugust 6, 2026 · 8 min

Stop losing webhooks.

Change one URL. Get retries, event log, and one-click replay.