---
title: "WP-Cron Isn't Enough: Build Reliable WordPress Webhooks"
description: "WP-Cron fires on page loads, not on a schedule. Webhooks drop silently under load. Build an async dispatch queue with retry logic — full architecture and code."
url: "https://wpwebhooks.org/blog/async-webhooks-wordpress-wp-cron-not-enough/"
date: "2026-03-03"
---

# WP-Cron Isn't Enough: Build Reliable WordPress Webhooks

TL;DR

-   WP-Cron fires only when someone visits the site — on low-traffic WordPress installs, scheduled jobs can be delayed by hours
-   Under load, concurrent page requests can trigger the same WP-Cron job simultaneously, causing duplicate webhook dispatches
-   Production webhook delivery needs a real system cron entry hitting `wp-cron.php` on a fixed interval, not WP-Cron

/ WP-Cron

## Why is **WP-Cron** not real background processing?

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 Problem

## Synchronous webhooks **block and silently fail**

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.

functions.php — the synchronous anti-pattern

```
// This runs inline during save_post — user waits for the HTTP call to complete.
add_action( 'save_post', function( $post_id ) {
    wp_remote_post( 'https://hooks.example.com/post-saved', [
        'body'    => wp_json_encode( [ 'post_id' => $post_id ] ),
        'headers' => [ 'Content-Type' => 'application/json' ],
    ] );
    // If the endpoint is down, this silently fails. No retry. No log.
} );
```

**The second problem is [silent failure](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/#symptoms).** 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.

/ Reliability

## Automation is **infrastructure**

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:

-   **Silent drops.** The webhook fires, the HTTP request times out, nothing is logged. The integration appears to work until you cross-reference source and destination records.
-   **Traffic-dependent execution.** Your automation only runs when users visit the site. Overnight batch events, low-traffic periods, or staging environments where no one browses produce gaps.
-   **No retry on transient failures.** A momentary endpoint outage, a cold-start delay on a serverless function, or a brief network hiccup permanently loses the event.
-   **Ordering problems.** Synchronous delivery doesn't queue — events fire in the order PHP encounters them, with no backpressure or rate limiting against the receiving endpoint.

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.

/ Architecture

## Separating event trigger from **job execution**

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](https://wpwebhooks.org/blog/wordpress-webhook-retry-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](https://wpwebhooks.org/blog/create-manage-wordpress-webhooks-rest-api/).

/ Queue Pattern

## A minimal **async queue** in WordPress

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:

1.  Hook fires
2.  Data saved to queue
3.  Background process starts
4.  Webhook dispatched
5.  Status updated

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_).

/ System Cron

## How does **system cron** compare to WP-Cron?

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.

crontab — run WordPress cron every minute via PHP CLI

```
# Run directly via PHP CLI — no HTTP overhead, no web server dependency.
# Replace the path with your actual WordPress installation path.
*/1 * * * * php /var/www/html/wp-cron.php
```

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:

wp-config.php — disable WP-Cron piggybacking on page loads

```
define( 'DISABLE_WP_CRON', true );
```

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:

crontab — trigger via REST endpoint with secret token

```
# Hit a REST endpoint that requires a token — keeps the trigger secure.
*/1 * * * * curl -s "https://your-site.com/wp-json/my-plugin/v1/run-queue?token=SECRET" > /dev/null 2>&1
```

Either approach gives you a deterministic, traffic-independent trigger. Your queue worker runs every minute regardless of whether anyone is browsing the site.

/ WooCommerce

## WooCommerce magnifies **every flaw**

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.

/ When To Use What

## WP-Cron: acceptable vs **not acceptable**

| 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.

/ Modern Architecture

## A practical architecture for **reliable WordPress webhooks**

A production-grade async webhook system for WordPress has seven layers. Each layer has a clear responsibility:

1.  **Event listener.** An `add_action` callback on the WordPress or WooCommerce hook that matters. Builds the payload and enqueues it. Returns immediately — no HTTP calls.
2.  **Queue storage.** A database table (or ActionScheduler) that holds jobs with status, payload, endpoint, attempt count, and scheduled dispatch time.
3.  **Dispatcher.** The worker function that reads pending jobs and calls `wp_remote_post`. Handles success (mark complete) and failure (schedule retry or move to dead-letter).
4.  **Retry logic.** Exponential backoff — each failed attempt reschedules the job further into the future. Prevents thundering-herd behaviour against a recovering endpoint.
5.  **Logging.** Per-attempt records: HTTP status code, response body (or error message), duration, timestamp. Essential for diagnosing failures — and queryable programmatically via the delivery log API for monitoring systems and automated recovery scripts.
6.  **Trigger.** A REST endpoint or WP-CLI command that the dispatcher runs on. Called by system cron on a fixed interval — every minute is typical.
7.  **System cron entry.** The OS-level scheduler that calls the trigger. Traffic-independent, predictable, the only reliable way to guarantee regular execution on a WordPress site.

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.

/ References

## Official **documentation**

All implementation patterns described here use WordPress-native APIs. These are the primary references:

-   [WP-Cron / Scheduling — how WP-Cron works, registering events, system cron alternatives](https://developer.wordpress.org/plugins/cron/)
-   [WordPress HTTP API — wp\_remote\_post, wp\_remote\_get, response handling](https://developer.wordpress.org/reference/functions/wp_remote_request/)
-   [wp\_remote\_post() — function reference, arguments, return values](https://developer.wordpress.org/reference/functions/wp_remote_post/)
-   [add\_action() — registering callbacks on WordPress action hooks](https://developer.wordpress.org/reference/functions/add_action/)
-   [do\_action() — how WordPress action hooks are fired](https://developer.wordpress.org/reference/functions/do_action/)
-   [WooCommerce Action Reference — full list of WooCommerce hooks and filters](https://woocommerce.com/document/introduction-to-hooks-actions-and-filters/)
-   [Action Scheduler — the robust job queue library bundled with WooCommerce](https://actionscheduler.org/)

/ Production Alternative

## If you'd rather not **maintain this yourself**

**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](https://wpwebhooks.org/webhook-wordpress-plugin-api/). If your team prefers configuration over maintaining custom queue infrastructure, you can explore the full details on the [async webhook plugin for WordPress](https://wpwebhooks.org/wordpress-webhook-plugin/). The code is publicly available on [GitHub](https://github.com/flowsystems-pl/wordpress-webhook-actions) and distributed via [WordPress.org](https://wordpress.org/plugins/flowsystems-webhook-actions/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WP-Cron Isn't Enough: Build Reliable WordPress Webhooks","description":"WP-Cron fires on page loads, not on a schedule. Webhooks drop silently under load. Build an async dispatch queue with retry logic — full architecture and code.","datePublished":"2026-03-03","dateModified":"2026-03-03","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/async-webhooks-wordpress-wp-cron-not-enough/","image":"https://wpwebhooks.org/blog/async-webhooks-wordpress-wp-cron-not-enough/og_image.jpg","keywords":["wordpress async webhooks","wp cron limitations","wordpress background processing","wordpress queue system","reliable webhooks wordpress"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WP-Cron Isn't Enough: Build Reliable WordPress Webhooks","item":"https://wpwebhooks.org/blog/async-webhooks-wordpress-wp-cron-not-enough/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is WP-Cron reliable enough for production webhook dispatch?","acceptedAnswer":{"@type":"Answer","text":"No. WP-Cron is not a real scheduler — it fires only when someone visits your WordPress site. On low-traffic sites, this means cron events may not run for minutes or hours. For production webhook dispatch, replace WP-Cron with a system cron entry that triggers the WordPress cron runner on a fixed schedule, regardless of site traffic."}},{"@type":"Question","name":"What's wrong with synchronous webhook dispatch in WordPress?","acceptedAnswer":{"@type":"Answer","text":"Synchronous dispatch 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. If the endpoint is slow, the page load is slow. If the endpoint times out, the event is silently lost — there is no retry, no log entry, and no signal that delivery failed."}},{"@type":"Question","name":"What does async webhook processing require in WordPress?","acceptedAnswer":{"@type":"Answer","text":"The minimal setup requires three components: a database table to store queued jobs, an 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. Add retry logic with exponential backoff and a logging layer to get a production-ready system."}},{"@type":"Question","name":"How does WooCommerce checkout performance relate to webhooks?","acceptedAnswer":{"@type":"Answer","text":"WooCommerce fires hooks like woocommerce_order_status_completed inline during the checkout process. If any listener on those hooks makes a synchronous outbound HTTP call, the checkout response is delayed by that call's latency. A slow or unavailable webhook endpoint can add seconds to every order — or cause checkouts to appear to fail. Async dispatch keeps webhook delivery entirely separate from the checkout flow."}}]}
```
