---
title: "WordPress Background Jobs: Async Queue Without Blocking"
description: "Process WordPress background jobs asynchronously — non-blocking dispatch, an Action Scheduler queue, retries with backoff, and reliable webhook delivery."
url: "https://wpwebhooks.org/blog/wordpress-async-background-processing/"
date: "2026-05-24"
---

# WordPress Background Jobs: Async Queue Without Blocking

TL;DR

-   `blocking => false` in `wp_remote_post` fires 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](https://developer.wordpress.org/plugins/cron/), and background job queues powered by [Action Scheduler](https://actionscheduler.org/). 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.

/ Non-Blocking Requests

## How do you make a **non-blocking fire-and-forget** request?

Pass `'blocking' => false` to any [`wp_remote_post`](https://developer.wordpress.org/reference/functions/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](https://developer.wordpress.org/plugins/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](https://wpwebhooks.org/blog/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.

/ 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](https://wpwebhooks.org/wordpress-webhook-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](https://wpwebhooks.org/blog/async-webhooks-wordpress/) for the end-to-end architecture, and [retry and replay](https://wpwebhooks.org/blog/wordpress-webhook-retry-replay-system/) for the backoff schedule and manual replay workflow.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WordPress Background Jobs: Async Queue Without Blocking","description":"Process WordPress background jobs asynchronously — non-blocking dispatch, an Action Scheduler queue, retries with backoff, and reliable webhook delivery.","datePublished":"2026-05-24","dateModified":"2026-05-24","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/wordpress-async-background-processing/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["wordpress async background processing","wordpress async request","wordpress queue job","wp remote post blocking","action scheduler async","wordpress background jobs","non blocking http 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":"WordPress Background Jobs: Async Queue Without Blocking","item":"https://wpwebhooks.org/blog/wordpress-async-background-processing/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What does blocking => false actually do in wp_remote_post?","acceptedAnswer":{"@type":"Answer","text":"It instructs WordPress's HTTP API to open the connection and send the request without waiting for a response. PHP returns control to your code immediately. The response body is never read, so you cannot check the HTTP status code or detect errors. Use it only when delivery confirmation is not required."}},{"@type":"Question","name":"Is a non-blocking wp_remote_post guaranteed to deliver?","acceptedAnswer":{"@type":"Answer","text":"No. If the remote server is unreachable or the OS has not finished flushing the TCP send buffer before PHP terminates, the request may not be sent at all. 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."}},{"@type":"Question","name":"Does WP-Cron run without traffic?","acceptedAnswer":{"@type":"Answer","text":"Not by default. WP-Cron fires on page load, so it depends on incoming HTTP requests to tick. On low-traffic sites the queue may sit idle for hours. The standard fix is to disable WP-Cron in wp-config.php (define('DISABLE_WP_CRON', true)) and call wp-cron.php from a real system cron every minute."}},{"@type":"Question","name":"What is the difference between WP-Cron and Action Scheduler for background jobs?","acceptedAnswer":{"@type":"Answer","text":"WP-Cron stores scheduled events in a single wp_options row with no attempt history, no retry, and no delivery log. Action Scheduler uses dedicated database tables, stores every action individually, retries failures automatically with exponential backoff, and keeps a full per-action log. For webhook delivery, Action Scheduler is the right choice."}},{"@type":"Question","name":"Can I use wp_remote_post for webhook delivery in production?","acceptedAnswer":{"@type":"Answer","text":"You can, but you need to add all the reliability infrastructure manually: enqueue the call outside the current request, add retry logic on 5xx responses, store attempt history, and build a replay mechanism. The Webhook Actions plugin provides all of this out of the box, built on Action Scheduler."}}]}
```
