TL;DR
- Action Scheduler stores jobs in a database table, not the transient-based WP-Cron option — jobs survive PHP crashes and are never lost on restart
action_scheduler_queue_runner_concurrent_batchescontrols parallel batch execution — default 5, tune based on server capacity- Webhook Actions has its own database queue for jobs; when Action Scheduler is available, WA uses it to trigger its queue runner — replacing WP-Cron with AS's more reliable loopback-based dispatch
/ Overview
What is Action Scheduler and how does it differ from WP-Cron?
Action Scheduler is a scalable, database-backed background job library for WordPress. It was originally developed by Automattic for WooCommerce Subscriptions, extracted into a standalone library, and is now bundled with WooCommerce core. It is used by WooCommerce, WooCommerce Payments, WooCommerce Subscriptions, and over 100 other plugins as the foundation for reliable background processing.
The core architectural difference from WP-Cron is storage. WP-Cron stores scheduled events in the _cron WordPress option as a serialized PHP array. This means all scheduled events are loaded into memory on every page load, updating a single option row creates write contention under concurrent traffic, and the data structure is not designed for per-job status tracking or queryability.
Action Scheduler stores every job in dedicated database tables: wp_actionscheduler_actions, wp_actionscheduler_logs, wp_actionscheduler_groups, and wp_actionscheduler_claims. Each job has its own row with a status column (pending, in-progress, complete, failed, canceled), a scheduled date, an attempt count, and a full execution log. This structure makes per-job tracking, concurrent processing, and failure recovery possible in ways WP-Cron cannot support.
| Feature | WP-Cron | Action Scheduler |
|---|---|---|
| Storage | options table (_cron) | Dedicated DB tables |
| Triggers | Page load (traffic-dependent) | WP-Cron + loopback HTTP requests |
| Per-job status | None | pending / in-progress / complete / failed |
| Concurrent processing | Single batch | Configurable parallel batches |
| Failure tracking | None | Per-action log with error message |
| Admin visibility | WP-CLI only | wp-admin/admin.php?page=action-scheduler |
| Survives PHP crash | Jobs may be lost | DB row survives, claimed jobs time out and retry |
For lightweight, infrequent tasks — a weekly digest email, a daily log rotation — WP-Cron with a real system cron entry is sufficient. Action Scheduler becomes the right choice when you need persistent job storage, per-job failure tracking, concurrent batch processing, or an admin UI for job inspection and management.
/ API
How does as_schedule_single_action work?
as_schedule_single_action() is the primary API function for scheduling a one-time background job. It inserts a row into wp_actionscheduler_actions and returns the action ID. The job fires once at the specified timestamp, then its status is updated to complete or failed. For the full parameter list, return values, and all related scheduling functions, see the Action Scheduler API Functions reference.
Function signature
/** * @param int $timestamp Unix timestamp when the action should run * @param string $hook The WordPress action hook to fire * @param array $args Arguments to pass to the hook callback * @param string $group Optional group name for organizing and querying actions * @return int The action ID */ as_schedule_single_action( $timestamp, $hook, $args = [], $group = '' );
Scheduling a webhook delivery job
// Schedule a webhook delivery job to run immediately (time = now). $action_id = as_schedule_single_action( time(), // fire as soon as the next queue runner cycle 'my_plugin_deliver_webhook', [ 'endpoint' => 'https://hooks.example.com/order', 'payload' => wp_json_encode( $payload ), 'attempt' => 1, ], 'my-plugin-webhooks' // group for admin UI filtering ); // Register the callback that fires when the action runs. add_action( 'my_plugin_deliver_webhook', function( $endpoint, $payload, $attempt ) { $response = wp_remote_post( $endpoint, [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => $payload, 'timeout' => 15, ] ); $code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) || $code >= 500 || $code === 429 ) { // Retry: schedule a new action at exponential backoff interval. if ( $attempt < 5 ) { $delay = 60 * ( 2 ** ( $attempt - 1 ) ); as_schedule_single_action( time() + $delay, 'my_plugin_deliver_webhook', [ 'endpoint' => $endpoint, 'payload' => $payload, 'attempt' => $attempt + 1 ], 'my-plugin-webhooks' ); } } }, 10, 3 );
The key design: each retry is a new as_schedule_single_action call, not a modification of the original action row. The original action row ends with status complete (the callback ran without throwing) even if the delivery failed — the retry logic is application-level, above the Action Scheduler abstraction. This is the pattern Webhook Actions uses internally.
Other scheduling functions follow the same signature pattern: as_schedule_recurring_action() for repeating jobs and as_enqueue_async_action() for jobs that should run as soon as possible on the next queue cycle.
/ Concurrency
What is action_scheduler_queue_runner_concurrent_batches?
action_scheduler_queue_runner_concurrent_batches is both a PHP constant and a filter that controls how many Action Scheduler batch processes can run simultaneously. The default value is 5. For the full tuning guide — when to raise it, when raising it 502s your site, and the relationship with action_scheduler_queue_runner_batch_size — see the Action Scheduler Concurrent Batches Filter & Tuning Guide.
Each "batch" is a separate PHP process initiated by Action Scheduler through a loopback HTTP request to wp-admin/admin-ajax.php. When the queue runner fires, it checks how many batches are currently in-progress and spawns additional batches up to the configured limit. Each batch claims a set of pending actions from the database, processes them in sequence, then exits.
This design means Action Scheduler can process multiple jobs simultaneously on servers where parallel PHP execution is available — which is true of virtually all WordPress hosting environments. A queue of 50 pending webhooks with 5 concurrent batches of 10 actions each can complete in roughly the time it takes to process a single batch of 10.
Queue runner fires (WP-Cron or system cron)
│
▼
Check: how many batches currently in-progress?
│
├─ Less than concurrent_batches limit
│ └─ Spawn additional batch via loopback HTTP
│
└─ At limit: skip (another runner fires next minute)
Each batch process:
├─ Claim N pending actions (lock via wp_actionscheduler_claims)
├─ Execute each action's hook callback
├─ Update status: complete / failed
└─ Release claim The claiming mechanism in wp_actionscheduler_claims prevents two concurrent batches from processing the same action. When a batch starts, it inserts a claim row for each action it intends to process. Other batches will not select actions that already have an active claim. If a batch crashes mid-execution, the claim row remains — a stuck-detection sweep resets these to pending after a configurable timeout.
/ Tuning
How do you control queue concurrency with the constant?
You can override the concurrent batch limit using either a PHP constant in wp-config.php or a WordPress filter. The constant takes precedence over the filter when both are present.
wp-config.php — set concurrency via constant
// Limit Action Scheduler to 3 concurrent batches. // Useful on shared hosting or memory-constrained environments. define( 'ACTION_SCHEDULER_QUEUE_RUNNER_CONCURRENT_BATCHES', 3 );
functions.php — set concurrency via filter
// Filter version — can be conditional (e.g. based on environment). add_filter( 'action_scheduler_queue_runner_concurrent_batches', function( $batches ) { return 3; } );
When to lower this value from the default of 5:
- Shared hosting — PHP worker pool is limited, typically 4–8 processes total. Five concurrent AS batches can exhaust the entire worker pool and block front-end requests.
- Memory-constrained VPS — each batch is a full WordPress bootstrap. At 128MB per process, five concurrent batches consume over 640MB of PHP memory before any plugin code runs.
- Database contention — high-frequency loopback requests creating claims on a busy
wp_actionscheduler_actionstable can cause MySQL lock contention. Reducing concurrency reduces claim write frequency.
When to raise this value:
- Dedicated VPS or cloud server — ample PHP workers and memory available; increasing to 10–20 batches dramatically increases webhook throughput under load.
- WooCommerce high-volume sites — large order volumes during flash sales or promotions generate many simultaneous webhook jobs; higher concurrency reduces queue drain time.
/ Queue Runner
What does the queue runner do and when does it fire?
The Action Scheduler queue runner is the process that inspects the wp_actionscheduler_actions table for overdue pending actions and dispatches them. It fires in two ways: via WP-Cron (traffic-dependent, unreliable on low-traffic sites) and via loopback HTTP requests that Action Scheduler itself initiates.
The WP-Cron trigger runs on a one-minute schedule. On each execution, the runner checks whether any batch slots are available, then initiates loopback HTTP requests for up to action_scheduler_queue_runner_concurrent_batches additional batch workers. Each batch worker is a separate PHP process that runs independently of the original cron trigger.
This means even with WP-Cron as the sole trigger, Action Scheduler can sustain high throughput: the cron fires once per minute, spawns five parallel batch workers, and each worker processes a batch of actions. The loopback workers run to completion independently — they do not depend on the original cron process staying alive.
For production reliability, the same advice applies as for WP-Cron: add a real system cron entry that calls the cron endpoint every minute. This decouples Action Scheduler's trigger from page load traffic. Without a real system cron, a low-traffic site at 3am may not trigger the queue runner for hours, leaving queued webhook jobs unprocessed.
/etc/cron.d/wordpress — real system cron for Action Scheduler
# Fire WP-Cron every minute — triggers Action Scheduler queue runner
* * * * * www-data curl -s https://yoursite.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1 WP-CLI provides direct queue runner access for development and debugging. wp action-scheduler run runs the queue runner once synchronously, processing one batch of pending actions. This is useful for testing without waiting for the cron cycle.
/ Webhook Integration
How does Action Scheduler power webhook retry queues?
Action Scheduler's properties map directly to the requirements for reliable webhook delivery. Database-backed persistence means a queued delivery survives PHP crashes. Per-job status means you can inspect every delivery attempt. Concurrent batches mean a high-volume WooCommerce store can drain its webhook queue quickly. The admin UI means you can manually retry a failed delivery without touching code.
The typical pattern for webhook delivery on top of Action Scheduler:
- Capture: On the WordPress action hook (WooCommerce order complete, CF7 submission, etc.), serialize the payload and call
as_schedule_single_action(time(), 'my_deliver_webhook', $args). The user-facing request ends here — no HTTP call made yet. - Dispatch: The queue runner picks up the action. The callback calls
wp_remote_post()and inspects the response code. - Retry on transient failure: On 5xx or timeout, the callback schedules a new
as_schedule_single_actionwith a calculated backoff delay and incremented attempt count. - Permanent failure: After the maximum attempt count, no new action is scheduled. The failure is logged in the Action Scheduler log with the final status code and error message.
WordPress action hook fires (order complete, form submit, etc.)
│
▼
Serialize payload
as_schedule_single_action(time(), 'deliver_webhook', $args)
│
▼
User-facing request completes ◄── no HTTP call made yet
│
(queue runner cycle, typically seconds later)
▼
deliver_webhook callback runs
│
├─ 2xx → complete (logged)
├─ 5xx/429 → as_schedule_single_action(now + backoff, ...)
└─ 4xx → fail permanently (logged) Webhook Actions applies a variation of this pattern: it maintains its own database queue for jobs rather than storing payloads in Action Scheduler action rows. What WA uses Action Scheduler for is triggering its queue runner — when AS is available, it registers an AS action to fire the queue runner on each cycle instead of relying on WP-Cron. The delivery log in the Webhook Actions admin panel is drawn from WA's own queue tables, not from the Action Scheduler log.
/ Monitoring
What are the failure modes and how do you monitor them?
Action Scheduler has four failure modes that are worth monitoring in production:
- Growing pending queue — The count of rows with
status = 'pending'grows continuously without draining. Cause: queue runner is not firing (WP-Cron not receiving traffic, system cron misconfigured) or the runner fires but all batch slots are occupied by slow workers. Check:wp action-scheduler list --status=pending --format=count. Growing past a few hundred warrants investigation. - Stuck in-progress actions — Actions with
status = 'in-progress'for longer than the batch timeout (default: 30 seconds per action). Cause: PHP crash, OOM kill, or server restart while a batch was running. The claim is never released. Fix: Action Scheduler's stuck-action detection sweeps these tofailedafter the timeout. - Accumulating failed actions — Spike in failed actions indicates a systematic endpoint problem: URL changed, authentication broke, or a code update broke the payload schema. Query:
wp action-scheduler list --status=failed --after=2026-05-07 --format=table. Inspect the error messages for patterns. - Table size growth — Completed and failed actions accumulate in
wp_actionscheduler_actionsandwp_actionscheduler_logsover time. Action Scheduler runs a cleanup job that prunes old completed actions (default: keep 30 days). If the table grows beyond tens of millions of rows, the cleanup interval or retention period may need tuning.
The Action Scheduler admin page at wp-admin/admin.php?page=action-scheduler provides visual status counts, filterable by status, hook, group, and date range. For automated monitoring, query the tables directly or use WP-CLI in a health-check script.
/ Webhook Actions
How does Webhook Actions integrate with Action Scheduler?
Webhook Actions maintains its own database-backed queue for outbound jobs — separate from the Action Scheduler tables. Every webhook trigger writes a job row to WA's queue rather than calling as_schedule_single_action(). The payload, attempt count, and retry state live in WA's own schema and are not stored in wp_actionscheduler_actions.
Where Action Scheduler comes in is queue triggering. WA checks at runtime whether Action Scheduler is available. If it is, WA registers an AS recurring action to fire its internal queue runner on each cycle. This replaces the WP-Cron trigger with AS's more reliable mechanism: loopback HTTP requests, concurrent batch dispatch, and AS's own stuck-action detection. If AS is not present, WA falls back to a WP-Cron scheduled event.
The practical result: on any site with WooCommerce (which ships AS), Webhook Actions automatically gets AS-quality trigger reliability without requiring you to configure anything. The delivery log in the Webhook Actions admin is sourced from WA's own queue tables, not from the Action Scheduler log — so wp action-scheduler list will show the recurring trigger action, not individual webhook delivery jobs.
Tuning ACTION_SCHEDULER_QUEUE_RUNNER_CONCURRENT_BATCHES affects how many concurrent AS batch workers fire WA's queue runner, which can increase the frequency of queue drain cycles on high-volume sites. The webhook delivery concurrency itself is controlled separately within WA's queue configuration.