Action Scheduler is the most reliable background job system available in WordPress. This article covers how it differs from WP-Cron, how as_schedule_single_action works, what action_scheduler_queue_runner_concurrent_batches controls, how the queue runner fires, and how Webhook Actions uses Action Scheduler to power reliable webhook delivery.
TL;DR
action_scheduler_queue_runner_concurrent_batches controls parallel batch execution — default 5, tune based on server capacityAction 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.
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.
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.
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.
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.
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.
When to lower this value from the default of 5:
wp_actionscheduler_actions table can cause MySQL lock contention. Reducing concurrency reduces claim write frequency.When to raise this value:
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.
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.
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:
as_schedule_single_action(time(), 'my_deliver_webhook', $args). The user-facing request ends here — no HTTP call made yet.wp_remote_post() and inspects the response code.as_schedule_single_action with a calculated backoff delay and incremented attempt count.
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.
Action Scheduler has four failure modes that are worth monitoring in production:
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.
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 to failed after the timeout. Check the sweep is running: wp action-scheduler list --hook=action_scheduler/cleanup_old_completed_actions.
wp action-scheduler list --status=failed --after=2026-05-07 --format=table. Inspect the error messages for patterns.
wp_actionscheduler_actions and wp_actionscheduler_logs over 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. Check row count: wp db query "SELECT COUNT(*) FROM wp_actionscheduler_actions".
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 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.
action_scheduler_queue_runner_concurrent_batches is a constant (and a filter) that controls how many Action Scheduler batch processes can run simultaneously. The default value is 5 on most configurations. Each batch is a separate PHP process initiated by a loopback HTTP request. Setting this to a higher value increases throughput for large queues but also increases server memory usage. Set it too high on a shared host and you will exhaust the PHP worker pool.
as_schedule_single_action($timestamp, $hook, $args, $group) schedules a one-time action to fire at $timestamp. It inserts a row into wp_actionscheduler_actions with status pending. When the Action Scheduler queue runner next executes and finds a pending action whose scheduled time is in the past, it claims the action (status → in-progress), fires the hook, and marks it complete or failed. The function returns the action ID.
woocommerce/action-scheduler) for plugins that don't depend on WooCommerce. When multiple plugins bundle different versions of Action Scheduler, WordPress automatically uses the newest version present — the versioning system is designed for this.
wp-admin/admin.php?page=action-scheduler. This admin page lists all actions with filters for status (pending, in-progress, complete, failed, canceled), hook name, group, and date range. You can manually run, cancel, or delete individual actions from this view. The page is available when WooCommerce is active — or when any plugin that bundles Action Scheduler registers it.
failed and records the error message in the log table. Failed actions are not automatically retried — you must explicitly reschedule them in your callback logic or use a retry-aware pattern.