TL;DR
blocking => falseinwp_remote_postfires the HTTP call without waiting for a response — but provides no retry or error visibility- WP-Cron defers work to the next page load, not a real timer — use a system cron entry to guarantee minute-level execution
- Action Scheduler stores jobs in DB tables, retries failures with exponential backoff, and keeps a full attempt log
- The Webhook Actions plugin uses Action Scheduler automatically — no manual queue code required for reliable webhook delivery
/ Overview
What is async background processing in WordPress?
"Async" in WordPress means decoupling work from the current HTTP request so the user's response returns immediately while the heavy work runs later — in a separate process, triggered by a queue runner or cron. Every email, order webhook, or file import that cannot fail silently needs this separation.
WordPress has three built-in mechanisms for background work: non-blocking HTTP requests (blocking => false), scheduled events via WP-Cron, and background job queues powered by Action Scheduler. Each has different guarantees and failure modes.
/ Why Blocking Fails
Why does synchronous HTTP block the WordPress response?
PHP executes synchronously within a single HTTP request. When WordPress calls wp_remote_post(), PHP opens a TCP connection, sends the request, and waits for the remote server to respond — all before returning the page to the visitor. A slow or unavailable endpoint can add seconds to page load, trigger PHP's execution timeout, and cause the triggering event to appear to fail even if WordPress itself completed successfully.
This is the root cause of most silent webhook failures. The admin action (status change, form submission) completes, but the downstream HTTP call times out or throws an exception that goes unlogged. No queue, no retry, no visibility.
SYNCHRONOUS (blocks request)
Browser WordPress Remote API
│ │ │
│── POST /checkout ─>│ │
│ │── wp_remote_post ─>│ ← PHP waits here
│ │ │ (could be 5-30 s)
│ │<─ 200 / timeout ──│
│<── response ──────│ │
│ (blocked until │ │
│ API responds) │ │ / Non-Blocking Requests
How do you make a non-blocking fire-and-forget request?
Pass 'blocking' => false to any wp_remote_post or wp_remote_request call. WordPress's HTTP API initiates the connection and returns control to PHP immediately, without reading the response body or status code.
PHP — non-blocking wp_remote_post
wp_remote_post( $url, [ 'method' => 'POST', 'body' => wp_json_encode( $payload ), 'headers' => [ 'Content-Type' => 'application/json' ], 'blocking' => false, // return immediately, don't read response 'timeout' => 0.01, // very short — we're not waiting anyway 'sslverify' => true, 'data_format' => 'body', ] );
Limitation: with blocking => false you get no response, no status code, no error information, and no retry if the endpoint is down. This pattern suits simple fire-and-forget notifications (logging, analytics pings) where missing one event is acceptable. For webhook delivery where every event must arrive, a persistent queue is necessary.
/ WP-Cron Deferred
How does WP-Cron defer background work outside the request?
WP-Cron is WordPress's built-in scheduler. You schedule a hook with wp_schedule_single_event( time() + 5, 'my_hook', $args ), then register a handler with add_action(). On the next page load after the scheduled time, WordPress fires the hook in a separate process — outside the current request but still driven by HTTP traffic.
The critical limitation: WP-Cron fires on page load, not on a real timer. A low-traffic site at 3 AM may wait hours before the next page load triggers the queue. Use a real system cron (*/1 * * * * curl -s https://site.com/wp-cron.php?doing_wp_cron) to get minute-level reliability. The WP-Cron developer reference covers the full function set.
/ Action Scheduler
How does Action Scheduler decouple jobs from the request cycle?
Action Scheduler goes further than WP-Cron: it stores every pending action in its own database tables, runs a dedicated queue runner via a system-level process (or as a WP-Cron job as fallback), and keeps a full attempt log per action. When an action fails, it is automatically retried with exponential backoff rather than silently dropped.
The queue runner claims a batch of pending actions, marks each as processing, executes the corresponding do_action() call, and marks the result complete or failed. Because claim locking is DB-level, multiple concurrent runners cannot process the same action twice — which is the core guarantee that makes async dispatch reliable at scale.
ASYNC WITH ACTION SCHEDULER
Browser WordPress Queue Runner
│ │ │
│── POST /checkout ─>│ │
│ │── as_enqueue_async ─> DB
│<── 200 ───────────│ (row inserted) │
│ (fast response) │ │
│ │ ┌─── pulls pending actions
│ │ │ fires do_action($hook)
│ │ │ calls wp_remote_post
│ │ └─── retries on 5xx / Queue Job Pattern
How do you implement a background queue job in WordPress?
The pattern has two parts: enqueue the work, and register the handler. With Action Scheduler, enqueuing is one line. The handler is a standard WordPress action callback — it can do any work that doesn't need to return a value to the calling request.
PHP — queue job pattern with Action Scheduler
// 1. Enqueue the job when the event fires add_action( 'woocommerce_payment_complete', function( $order_id ) { as_enqueue_async_action( 'my_plugin_send_crm_webhook', [ 'order_id' => $order_id ], 'webhooks', true ); } ); // 2. Handle the job in the queue runner process add_action( 'my_plugin_send_crm_webhook', function( $order_id ) { $order = wc_get_order( $order_id ); $payload = [ 'email' => $order->get_billing_email(), /* ... */ ]; $response = wp_remote_post( $crm_url, [ 'body' => wp_json_encode( $payload ), 'headers' => [ 'Content-Type' => 'application/json' ], ] ); if ( is_wp_error( $response ) ) { throw new Exception( $response->get_error_message() ); // Action Scheduler catches the exception and retries } } );
/ Comparison
Blocking PHP vs async background dispatch: what changes?
| Concern | Blocking wp_remote_post | Webhook Actions (async queue) |
|---|---|---|
| Response time | Adds endpoint latency to every page load | Returns immediately — job runs in queue runner |
| Retry on failure | None — one attempt, silent drop | Exponential backoff: 1 m → 2 m → 4 m → 8 m, 5 attempts |
| Error visibility | WP_Error swallowed unless you log it yourself | Attempt log with HTTP status and response body |
| Delivery guarantee | Best-effort — timeout or 5xx = lost | Persistent queue — survives PHP crash or server restart |
| Request isolation | Failure aborts the current user request | Job failure never affects the triggering request |
/ Plugin Option
How does Webhook Actions handle async dispatch automatically?
The Webhook Actions plugin wraps the entire queue-job pattern described above. You bind a hook to an endpoint in wp-admin; the plugin enqueues every triggered delivery via Action Scheduler (or WP-Cron as fallback), injects identity headers (X-Event-Id, X-Event-Timestamp, X-Webhook-Id), retries 5xx failures with exponential backoff, and logs every attempt in a queryable delivery log — all without writing PHP.
Custom payload shaping is available via the fswa_webhook_payload filter or via Code Glue snippets (Pro). For integrations that need conditional dispatch, the fswa_should_dispatch filter lets you skip delivery based on payload fields, order status, or any WordPress state — keeping the queue clean rather than delivering and discarding on the receiving end.
See the async webhooks article for the end-to-end architecture, and retry and replay for the backoff schedule and manual replay workflow.