Synchronous PHP blocks every page load. This article covers non-blocking wp_remote_post, WP-Cron deferred execution, and Action Scheduler persistent job queuing — with the trade-offs that decide which tool belongs in production.
TL;DR
"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.
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) │ │
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.
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 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 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
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.
| 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 |
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.
blocking => false is fire-and-forget at the PHP layer, not a guaranteed-delivery mechanism. For reliable delivery, use Action Scheduler with a persistent queue and automatic retry.
define('DISABLE_WP_CRON', true)) and call wp-cron.php from a real system cron every minute.