WP-Cron is not real background processing — it only runs when someone visits your site. Synchronous webhook dispatch blocks PHP and silently loses events. Async queues fix both problems: they decouple event capture from delivery, add retries, and keep your checkout fast.
When developers first encounter WP-Cron, it looks like a background task system. You schedule an event with wp_schedule_event, attach a callback with add_action, and WordPress "runs it in the background." Except it doesn't.
WP-Cron fires on page load. Every time a request hits your WordPress site, WordPress checks whether any scheduled events are past due and runs them inline — piggybacking on the visitor's request. There is no daemon, no OS-level scheduler, no separate process. The "cron" is entirely simulated.
This design has a direct consequence: if no one visits your site, WP-Cron does not run. A webhook job scheduled for 2:00 AM might not execute until the first visitor at 9:00 AM. On a staging environment with no traffic, cron events may never fire at all.
There are also race conditions. On high-traffic sites, multiple concurrent requests can each detect the same overdue event and attempt to run it simultaneously. WordPress uses a transient-based lock to mitigate this, but the lock is not guaranteed under heavy load — jobs can fire multiple times.
These are not edge cases. They are the default behavior. Any automation that depends on WP-Cron for timely, exactly-once execution is fragile by design.
The most common webhook pattern in WordPress looks like this: attach a callback to an action hook, and inside that callback, fire wp_remote_post to notify an external service. It works in development. In production it causes two categories of problems.
The first problem is blocking. PHP is single-threaded. When wp_remote_post runs, execution pauses until the remote endpoint responds or the request times out. Every millisecond the external service takes is a millisecond added to the user's page load. If you're dispatching webhooks inside WooCommerce checkout hooks, you are directly coupling checkout latency to the availability of a third-party API.
The second problem is silent failure. If the endpoint times out, the WordPress default HTTP timeout is 5 seconds — the request is abandoned and the data is gone. There is no retry mechanism, no error log entry visible to the site owner, and no indication that the event was never delivered. Your CRM or automation platform simply never received the notification.
These two problems compound. A slow endpoint makes your site slow. A down endpoint makes your site slow and loses the event. Neither is acceptable in any production automation workflow.
A webhook that sometimes delivers is not an integration — it's a liability. When your WooCommerce orders trigger CRM updates, fulfillment notifications, or accounting entries, a missed event means a broken business process. The failure is often invisible until a customer complains or a reconciliation reveals the gap.
Fragile webhook delivery creates a specific category of failure modes that are hard to diagnose and expensive to recover from:
None of these are hypothetical. They manifest on real WordPress and WooCommerce installations once volume or business criticality crosses a threshold. The fix is an architectural change, not a tweak to existing code.
WordPress Action Hook fires (e.g. woocommerce_order_status_completed)
│
▼
Listener: writes job to queue table
{ endpoint, payload, status=pending, attempt=0 }
│
▼
Response sent to user ◄── request cycle ends here
│
(background — decoupled from user request)
▼
Cron Worker (system cron or WP-Cron)
│
├── Fetch pending jobs where scheduled_at <= now
│
▼
wp_remote_post( endpoint, payload )
│
├─ 2xx → mark job complete, log success
│
└─ fail → increment attempt, schedule retry (exponential backoff)
│
└─ attempt >= max → move to dead-letter, alert
The key insight is the separation of concerns. The action hook listener does one thing: record the event. It writes a row to a database table and returns immediately. The user's PHP request is never held open by an outbound HTTP call.
Event trigger. Any WordPress or WooCommerce action hook. The listener captures the data it needs, serialises it as JSON, and inserts a queue row. This is fast — a single database write.
Job storage. A dedicated database table, not the options or postmeta tables. Querying by status and scheduled time is efficient against a properly indexed custom table; it is impractical against postmeta at scale.
Job execution. A background worker, triggered on a regular schedule, reads pending jobs and attempts delivery. It updates the job record on success, reschedules on failure using exponential backoff, and moves permanently failed jobs to a dead-letter state. Every attempt is logged.
This architecture gives you non-blocking dispatch, automatic retry, structured logging, and rate-limiting capability — none of which are possible with synchronous wp_remote_post in a hook callback. For a deeper look at how retry and replay fit together into a complete delivery system, see Inside My Webhook Replay System. For managing the webhook definitions themselves — creating, updating, toggling, and deleting webhooks without touching wp-admin — see Create and Manage WordPress Webhooks Programmatically.
WordPress does not ship with a native job queue. ActionScheduler (bundled with WooCommerce) provides one, and there are several standalone options. If you're building custom infrastructure, the core pattern is straightforward: a database table, a listener, and a worker.
The flow from event to delivery looks like this:
Steps 1 and 2 happen synchronously inside the user's request — they are fast (one DB write) and have no external dependencies. Steps 3 through 5 happen in the background, completely outside the user's request cycle.
Common approaches for the queue storage layer include a custom $wpdb table (most control, most work), ActionScheduler's existing tables (already present on WooCommerce sites), or a lightweight plugin that provides a queue API. The choice depends on how much control and how much maintenance overhead you want.
Whichever storage layer you choose, the contract is the same: the listener writes, the worker reads, and the queue tracks state transitions from pending → complete (or failed).
Even if your queue architecture is correct, it still needs a reliable trigger. WP-Cron is not that trigger. As described earlier, WP-Cron depends on page visits — if no one loads a page, the worker doesn't run. Jobs accumulate in the queue, unprocessed.
The fix is to use the operating system's real scheduler — cron on Linux — to run the WordPress cron handler on a fixed interval. This is traffic-independent, predictable, and requires no code changes to your queue or worker logic.
Disable the default WP-Cron behaviour in wp-config.php once system cron is configured, so that page loads no longer trigger the cron check:
If direct CLI access is not available, an alternative is a token-protected REST endpoint that triggers the cron runner. The system cron then calls that endpoint via curl or wget. This approach works on managed hosting environments where you only have HTTP access:
Either approach gives you a deterministic, traffic-independent trigger. Your queue worker runs every minute regardless of whether anyone is browsing the site.
WooCommerce is where WP-Cron and synchronous webhook problems are felt most acutely. Checkout is a high-stakes, high-frequency operation. Customers are completing purchases — any latency or failure directly affects conversion rates and revenue.
WooCommerce fires a cascade of action hooks during order processing: woocommerce_checkout_order_created, woocommerce_payment_complete, woocommerce_order_status_completed, and others. If any listener on these hooks makes a synchronous outbound HTTP call, that call's latency is added to the checkout response time.
At low order volumes, the effect is imperceptible. At moderate volumes — dozens of orders per hour — a 500ms average webhook call adds meaningful latency to every checkout. At high volumes, or when an external endpoint has a bad moment, checkout pages begin to hang or return errors.
Async dispatch solves this completely. The checkout hook listener writes a queue row (fast), the response is sent to the customer immediately, and the webhook delivery happens in the background. Checkout speed is never coupled to the availability or latency of a third-party endpoint.
WooCommerce also has the ActionScheduler library built in — a robust, database-backed job queue. If you're building on WooCommerce, you can use ActionScheduler directly rather than writing your own queue table. The pattern is the same; the infrastructure is already there.
| Use Case | WP-Cron OK | WP-Cron Not OK |
|---|---|---|
| Low-traffic hobby site | ✓ | — |
| Non-critical background tasks | ✓ | — |
| Email digests & newsletters | ✓ | — |
| Revenue workflows | — | ✗ |
| CRM sync & lead routing | — | ✗ |
| Payment-related automation | — | ✗ |
The distinction is consequence. If a missed event means a content cache takes an extra hour to clear, WP-Cron's unreliability is tolerable. If a missed event means an order isn't synced to your ERP or a customer isn't enrolled in a course they paid for, it is not.
WP-Cron's reliability is also proportional to traffic. High-traffic sites where every page load is a potential cron trigger get close to minute-by-minute execution. Low-traffic or staging sites can go hours without a trigger. Build your architecture for the worst case, not the average.
A production-grade async webhook system for WordPress has seven layers. Each layer has a clear responsibility:
add_action callback on the WordPress or WooCommerce hook that matters. Builds the payload and enqueues it. Returns immediately — no HTTP calls.wp_remote_post. Handles success (mark complete) and failure (schedule retry or move to dead-letter).Layers 1–5 are application code. Layers 6–7 are infrastructure configuration. On managed WordPress hosting where you don't control the OS cron, managed queues or a plugin that handles its own scheduling become the practical alternative.
All implementation patterns described here use WordPress-native APIs. These are the primary references:
Flow Systems Webhook Actions is an open-source async webhook plugin for WordPress that implements this architecture out of the box — including queue processing, retry logic with exponential backoff, and structured delivery logging. Delivery logs, retry triggers, and queue status are also accessible programmatically via the REST API. If your team prefers configuration over maintaining custom queue infrastructure, you can explore the full details on the async webhook plugin for WordPress. The code is publicly available on GitHub and distributed via WordPress.org.
define( 'DISABLE_WP_CRON', true ); to wp-config.php to prevent the default page-load trigger from interfering.
wp_remote_post directly inside an action hook callback — blocks the PHP thread while waiting for the remote endpoint to respond. The user's request is held open until the HTTP call completes or times out.add_action listener that writes to that table when a WordPress event fires, and a background worker triggered by WP-Cron or system cron that reads from the table and dispatches via wp_remote_post.woocommerce_order_status_completed and woocommerce_payment_complete inline during the checkout and order-processing flow. If any listener on those hooks makes a synchronous outbound HTTP call, that call's latency is directly added to the checkout response time.