---
title: "Action Scheduler Concurrent Batches: Filter & Tuning Guide"
description: "Tune action_scheduler_queue_runner_concurrent_batches and the batch-size filter to push Action Scheduler past WP-Cron throughput limits — full reference."
url: "https://wpwebhooks.org/blog/action-scheduler-concurrent-batches-filter/"
date: "2026-05-17"
---

# Action Scheduler Concurrent Batches: Filter & Tuning Guide

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](https://github.com/woocommerce/action-scheduler/blob/trunk/classes/ActionScheduler_QueueRunner.php). 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.

/ 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](https://actionscheduler.org/admin/) 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. Raising it also widens the window for [MySQL deadlocks on the claim query](/blog/woocommerce-action-scheduler-mysql-deadlocks/) when multiple runners collide.

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](https://actionscheduler.org/). 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](/blog/cron-job-for-wordpress/) 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](/blog/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.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"Action Scheduler Concurrent Batches: Filter & Tuning Guide","description":"Tune action_scheduler_queue_runner_concurrent_batches and the batch-size filter to push Action Scheduler past WP-Cron throughput limits — full reference.","datePublished":"2026-05-17","dateModified":"2026-05-17","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/action-scheduler-concurrent-batches-filter/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["action scheduler queue runner concurrent batches","action scheduler concurrent batches","action scheduler queue runner batch size","action scheduler tuning","woocommerce background processing","wp cron throughput"]}

{"@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":"Action Scheduler Concurrent Batches: Filter & Tuning Guide","item":"https://wpwebhooks.org/blog/action-scheduler-concurrent-batches-filter/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is action_scheduler_queue_runner_concurrent_batches a constant or a filter?","acceptedAnswer":{"@type":"Answer","text":"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. You hook the filter in PHP — typically from an mu-plugin — and return an integer."}},{"@type":"Question","name":"What is the default value?","acceptedAnswer":{"@type":"Answer","text":"Since Action Scheduler 3.5 the default is 5. Earlier versions defaulted to 1. Check your installed version with wp plugin get woocommerce --field=version because Action Scheduler ships inside WooCommerce."}},{"@type":"Question","name":"How is action_scheduler_queue_runner_batch_size different?","acceptedAnswer":{"@type":"Answer","text":"Batch size is the number of actions processed per batch (default 25). Concurrent batches is the number of parallel batches per cron tick (default 5). Throughput per tick equals batch_size × concurrent_batches. Tune both together — raising one without the other hits the wrong bottleneck."}},{"@type":"Question","name":"Why does raising concurrent batches return 502 errors?","acceptedAnswer":{"@type":"Answer","text":"Each parallel batch consumes one PHP-FPM worker via an internal loopback request. If concurrent_batches × batch_duration exceeds your available PHP workers, real visitor requests have no worker to serve them and the front-end load balancer returns 502. Lower the value or raise pm.max_children."}},{"@type":"Question","name":"How can I check the effective value on a live site?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}
```
