/ Reference — Action Scheduler

Action Scheduler Concurrent Batches: Filter & Tuning Guide

Action Scheduler ships with conservative concurrency defaults. The action_scheduler_queue_runner_concurrent_batches filter is the lever that decides how many parallel batches each cron tick runs — and how fast your queue actually drains.

TL;DR

  • action_scheduler_queue_runner_concurrent_batches is a filter, not a constant — return an integer (default since AS 3.5 is 5).
  • It controls how many parallel loopback requests Action Scheduler fires per cron tick. Pair it with action_scheduler_queue_runner_batch_size (default 25) to multiply throughput.
  • Raising it on shared hosting will exhaust PHP workers and 502 your site — tune only when you control PHP-FPM pool size and have a real queue backlog.
/ Definition

What does action_scheduler_queue_runner_concurrent_batches actually do?

It caps the number of parallel batches the Action Scheduler queue runner fires per cron tick. Each batch is a separate wp_remote_post loopback request hitting admin-post.php?action=as_async_request_queue_runner on your own site, which runs one PHP process that drains up to batch_size actions before exiting.

With the default of 5 concurrent batches and 25 actions per batch, one cron tick can dispatch up to 125 actions. With the historical default of 1 batch, it could only dispatch 25. On a site queuing 10,000 actions per hour (high-volume WooCommerce, bulk email, sync jobs), the 1-batch ceiling is exactly why the queue grows faster than it drains.

The filter fires inside ActionScheduler_QueueRunner::has_maximum_concurrent_batches() — see the QueueRunner source on GitHub. It runs every time the cron tick evaluates whether to dispatch another loopback, so the value is read live, not cached. Changes take effect on the next tick.

  WordPress / WooCommerce
         │
         ▼
  Action Scheduler queue (DB-backed)
  pending  pending  pending  pending  pending  ...
         │
         ▼
  Queue Runner cron tick (every minute)
         │
         ▼
  ┌──────────── concurrent_batches = 1 (default) ────────────┐
  │ Batch A: 25 actions ──► one PHP loopback request         │
  └──────────────────────────────────────────────────────────┘

  ┌──────────── concurrent_batches = 5 (tuned) ──────────────┐
  │ Batch A: 25 actions ──► loopback request #1              │
  │ Batch B: 25 actions ──► loopback request #2              │
  │ Batch C: 25 actions ──► loopback request #3              │
  │ Batch D: 25 actions ──► loopback request #4              │
  │ Batch E: 25 actions ──► loopback request #5              │
  │ Total: 125 actions per tick (5× throughput)              │
  └──────────────────────────────────────────────────────────┘
/ Filter vs Constant

Is it a filter or a constant?

It is a filter, applied via apply_filters( 'action_scheduler_queue_runner_concurrent_batches', $batches ). There is no PHP constant by that name and defining one in wp-config.php has no effect. Misreading the GSC search query as a constant is the most common mistake on Stack Overflow.

The related action_scheduler_queue_runner_batch_size is also a filter, not a constant. Both must be hooked in PHP — typically from an mu-plugin so the values survive plugin deactivation and theme switches.

/ Tuning

How do I raise the concurrent-batch limit safely?

Start by confirming there is a real backlog. Open WooCommerce → Status → Scheduled Actions (the Action Scheduler admin docs describe the columns) or run wp action-scheduler queue-status, and check the Pending count. If pending is consistently under 100 and the oldest pending action is under five minutes old, the queue is already draining — tuning will burn CPU for no throughput gain.

If pending is in the thousands and growing, raise the values together. concurrent_batches × batch_size equals the maximum actions processed per cron tick. Doubling concurrent batches without raising batch size produces only marginal gains because most batches finish quickly and exit.

Each parallel batch consumes one PHP-FPM worker for the duration of the batch. With PHP-FPM pm.max_children = 10 and concurrent_batches = 5, half your worker pool can be tied up draining actions — leaving five workers for actual page loads. Below 10 workers, leave the default. Above 30 workers, raising to 5 (the modern default) is safe.

mu-plugins/action-scheduler-tuning.php — production-safe defaults
// wp-content/mu-plugins/action-scheduler-tuning.php // Raise Action Scheduler throughput on a host that can handle it. add_filter( 'action_scheduler_queue_runner_concurrent_batches', function() { return 5; // default 5 since AS 3.5; lower it to 1 on shared hosting } ); add_filter( 'action_scheduler_queue_runner_batch_size', function() { return 50; // actions processed per batch; default 25 } ); add_filter( 'action_scheduler_queue_runner_time_limit', function() { return 30; // seconds; stay well under PHP max_execution_time } );
/ Companion Filter

What is the relationship with action_scheduler_queue_runner_batch_size?

Batch size is actions per batch; concurrent batches is the number of batches per tick. Throughput per minute equals roughly batch_size × concurrent_batches × ticks_per_minute. Raising one without the other hits the wrong bottleneck.

Raise batch_size when individual actions are fast (a few HTTP calls or DB writes) and the per-batch startup overhead dominates. Raise concurrent_batches when individual actions are slow (long-running imports, large emails) and you want to parallelize across PHP workers.

The hard ceiling on batch_size is your PHP max_execution_time. If your batches consistently hit the action_scheduler_queue_runner_time_limit (30s by default) and exit early, lower batch_size instead of raising the time limit — early-exit batches re-enter the queue cleanly, but PHP max_execution_time kills leave actions in a processing state that needs manual recovery.

/ Inspection

How do I see the current values on a live site?

Run wp eval "echo apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 5 );" from the site root. The output is the effective value after all hooked filters apply. Run the same against action_scheduler_queue_runner_batch_size with a default of 25.

For the live throughput picture, run wp action-scheduler run --batches=1 and read the action count and elapsed time. Multiply by the tick frequency (60 ticks/hour on a one-minute cron) and compare against your enqueue rate. If enqueue exceeds throughput, the backlog will grow no matter how patiently you wait.

/ Hosting Pitfalls

Which hosting environments break when you raise these?

Shared hosting with low pm.max_children values (Bluehost, GoDaddy, SiteGround starter plans). Raising concurrent batches above 1 fills the PHP worker pool with self-issued loopback requests and returns 502 to real visitors. Symptom: site goes down at the top of every minute when the cron tick fires.

Hosts that block loopback requests (some managed WordPress hosts, Cloudflare with strict firewall rules). Action Scheduler's loopback dispatch silently fails when admin-post.php?action=as_async_request_queue_runner is blocked, falling back to in-process execution that ignores the concurrent batches filter entirely.

PHP-FPM with pm = ondemand and aggressive pm.process_idle_timeout. Concurrent batches create burst load, FPM spawns workers, then kills them — paying the worker startup cost on every tick. Use pm = dynamic with a sensible pm.min_spare_servers instead.

On managed WordPress hosting (Kinsta, WP Engine, Pressable), check the host's docs before raising concurrent batches. Many enforce their own queue runner via a system cron and document recommended values — overriding them risks support deflection ("we don't support custom Action Scheduler tuning").

/ WP-Cron Interaction

How does this interact with WP-Cron and loopback requests?

Action Scheduler's queue runner is dispatched by WP-Cron via the action_scheduler_run_queue scheduled event — confirmed in the Action Scheduler architecture docs. If WP-Cron does not fire — because no page request triggered it, or because DISABLE_WP_CRON is set without a system cron — the queue runner never starts and the concurrent_batches filter is never consulted.

On a low-traffic site, the realistic first fix is not raising concurrent_batches. It is replacing page-load-triggered WP-Cron with a system cron entry hitting wp-cron.php every minute. See the WP-Cron replacement guide for the exact crontab line.

Once a real cron is firing the queue runner reliably, each parallel batch spawned by the concurrent_batches filter issues a second internal HTTP request — a loopback to admin-post.php. Five concurrent batches = five concurrent loopbacks per tick. This is the layer that consumes PHP workers and trips host rate limits.

/ When to Stop

When should you stop tuning and switch to a real queue?

When you are tuning these filters more than once a quarter, the workload has outgrown Action Scheduler. The signal is consistent: pending stays above 5,000 even after tuning, the runner spends most of its time on retries, and you have started adding custom cron entries to run the queue runner more often than every minute.

At that point the next step is offloading dispatch to a real queue (Redis, SQS, RabbitMQ) with a dedicated worker process outside the PHP request cycle — the same architecture covered in Why WordPress Webhooks Silently Fail in Production. The Webhook Actions plugin sits in between: it ships its own persistent queue tables with smart retry routing and a delivery log, and uses Action Scheduler as the runner when available (falling back to WP-Cron). You configure concurrency once, then operate from the admin console.

/ FAQ

Common questions

It is a filter, applied via apply_filters( 'action_scheduler_queue_runner_concurrent_batches', $batches ). There is no constant by that name; defining one in wp-config.php has no effect.

Hook it in PHP from an mu-plugin and return an integer. Changes take effect on the next cron tick.
Since Action Scheduler 3.5 the default is 5. Earlier versions defaulted to 1.

Check the installed version with wp action-scheduler --version or by inspecting wp-content/plugins/woocommerce/packages/action-scheduler/.
batch_size is the number of actions per batch (default 25). concurrent_batches is the number of parallel batches per cron tick (default 5).

Throughput per tick = batch_size × concurrent_batches. Raise batch size when actions are fast; raise concurrent batches when actions are slow and you want parallelism across PHP workers.
Each parallel batch consumes one PHP-FPM worker via an internal loopback request to admin-post.php. If the active batches outnumber your free workers, real visitor requests have nothing to serve them and the load balancer returns 502.

Fix: lower concurrent_batches, raise pm.max_children, or both. On shared hosting, leave the value at 1.
Run wp eval "echo apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 5 );" from the site root. The output is the effective value after every hooked filter runs.

Do the same with action_scheduler_queue_runner_batch_size defaulting to 25.