WP Webhooks / Blog / WordPress internals
Article · WordPress internals

How action_scheduler_run_queue Drives Every Queue Pass

action_scheduler_run_queue is the WP-Cron hook behind every Action Scheduler queue pass: how it is registered, its every_minute schedule, and how to trigger it.

8 min 2026-07-26
#Action Scheduler#WP-Cron

TL;DR: action_scheduler_run_queue is the WP-Cron hook that starts every Action Scheduler queue pass.

  • Action Scheduler registers it as a recurring event on its own every_minute schedule (a 60-second interval it adds itself).
  • When the hook fires, ActionScheduler_QueueRunner::run() executes with the context string WP Cron.
  • One pass claims a batch of 25 actions and runs for at most 30 seconds, then may fire an async loopback request to keep draining.
  • The hook being scheduled is not the same as the hook firing — if WP-Cron never runs, neither does the queue.

/ Definition

What is action_scheduler_run_queue?

It is the name of the WordPress cron hook that Action Scheduler schedules for itself, and the entry point for all queue processing. Nothing in Action Scheduler runs on a timer of its own: the library leans on WP-Cron for its heartbeat, and action_scheduler_run_queue is the hook that heartbeat calls.

That makes it the single most useful symbol to know when a queue is stuck. If scheduled actions are piling up as pending past their due time, the question is almost always "is action_scheduler_run_queue firing?" — not "is the action broken?".

The dispatch path of the action_scheduler_run_queue hookAction Scheduler registers a recurring WP-Cron event on the hook action_scheduler_run_queue using its own every_minute schedule. When WP-Cron fires that hook, the queue runner run method executes with the context string WP Cron. It first checks whether another batch is already running, and stops if the concurrent batch limit is reached. Otherwise it claims a batch of due actions, executes each hook until the time limit or batch size is reached, then releases the claim and may fire an asynchronous loopback request to continue draining the queue before the next minute tick.

WP-Cron fires the hook

yes

no

yes

no

ActionScheduler_QueueRunner::init()
wp_schedule_event on every_minute

WP-Cron event
action_scheduler_run_queue

QueueRunner::run( 'WP Cron' )

concurrent batch
limit reached?

exit, wait for next tick

claim a batch
default 25 actions

execute each action
until the 30s time limit

release the claim

work still due?

async loopback request
runs another pass

FIG 01 — How action_scheduler_run_queue starts a queue pass

/ Registration

How is the hook registered and scheduled?

In ActionScheduler_QueueRunner, as two class constants and an init() method that wires them up. The hook name and its schedule are fixed in the source:

PHP — ActionScheduler_QueueRunner (simplified)

const WP_CRON_HOOK = 'action_scheduler_run_queue';
const WP_CRON_SCHEDULE = 'every_minute';

public function init() {
    // Add the custom 60-second interval to WP-Cron's schedules.
    add_filter( 'cron_schedules', [ $this, 'add_wp_cron_schedule' ] );

    $cron_context = [ 'WP Cron' ];

    if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) {
        wp_schedule_event( time(), self::WP_CRON_SCHEDULE, self::WP_CRON_HOOK, $cron_context );
    }

    add_action( self::WP_CRON_HOOK, [ self::instance(), 'run' ] );
}

Three things are worth pulling out. The every_minute schedule is not a WordPress built-in — Action Scheduler adds it through the cron_schedules filter with an interval of 60 seconds. The event is registered through wp_schedule_event() with an argument array of ['WP Cron'], which is why a raw look at the cron array shows the hook carrying one argument. And the callback is the queue runner's run() method, not a standalone function.

That argument array matters more than it looks. Because wp_next_scheduled() matches on hook name and arguments, an event scheduled with different args is a different event — which is how a manual do_action() call and the cron-driven pass can be told apart in logs.

/ Dispatch

What happens when the hook fires?

ActionScheduler_QueueRunner::run() executes with the context it was scheduled with, and attempts one queue pass:

PHP — the callback signature

public function run( $context = 'WP Cron' ) {
    // 1. Bail if the concurrent-batch limit is already reached.
    // 2. Claim a batch of due actions (default 25).
    // 3. Execute each action until the time limit (default 30s).
    // 4. Release the claim.
}

The $context string is purely descriptive — it identifies what triggered this pass, and shows up in the logs written to wp_actionscheduler_logs. You will see WP Cron for the scheduled event, Async Request for a loopback pass, and WP CLI when a pass is driven from the command line. It does not change the work performed.

Step 1 is the one that surprises people. Before doing anything, the runner checks whether another batch is already in flight, and exits immediately if the concurrency limit is hit. By default that limit is one, so on a site where a long-running action is still executing, the next minute's tick does nothing at all. That is deliberate — it protects the database from parallel claim contention — and it is tunable, as covered in the concurrent batches filter reference.

/ Limits

Which filters change what a pass does?

Four, and their defaults are documented in the Action Scheduler performance guide:

FilterDefaultWhat it controls
action_scheduler_queue_runner_batch_size25How many actions a single claim locks in one batch.
action_scheduler_queue_runner_concurrent_batches1How many batches may run at the same time across all runners.
action_scheduler_queue_runner_time_limit30Seconds one pass may spend executing before it stops claiming more work.
action_scheduler_run_queueNot a filter — the cron hook itself. Fire it with do_action() to force a pass.

The time limit is why raising the batch size alone rarely helps: a pass that runs out of its 30 seconds stops mid-batch regardless of how many actions it claimed. Throughput on a backed-up queue comes from more passes or more concurrency, not bigger batches. The rows those batches lock live in the claims table — see the Action Scheduler database schema for how a claim_id is minted and released.

The queue runner is triggered at most once every minute. Everything beyond that first pass comes from asynchronous loopback requests, not from cron. — Action Scheduler performance guide

/ Loopback

How does the async loopback runner fit in?

It is how Action Scheduler drains a backlog faster than one batch per minute. A once-a-minute cron tick that processes 25 actions caps you at 1,500 actions an hour — nowhere near enough for a store generating thousands of jobs. So when a cron-triggered pass finishes and work is still due, the runner dispatches an asynchronous loopback request to the site, which runs another pass immediately.

The practical consequence: loopback requests are HTTP requests your site makes to itself. If they are blocked — by HTTP authentication, a firewall rule, a misconfigured host, or a plugin that intercepts loopbacks — the queue silently falls back to the once-a-minute cron rate. A backlog that shrinks at exactly 25 actions per minute is the signature of blocked loopbacks, not of a slow database.

/ Troubleshooting

Why is the hook scheduled but nothing runs?

Because a scheduled WP-Cron event is only a promise. WP-Cron is triggered by traffic: on each page load WordPress checks whether anything is due and spawns a request to run it. No traffic, no tick — and no queue pass. Three failure modes cover almost every case:

  1. DISABLE_WP_CRON is true with no replacement. Defining the constant switches off the page-load trigger. If nothing calls wp-cron.php on a schedule after that, action_scheduler_run_queue is scheduled forever and fired never. The fix is a real system cron entry, covered in the WP-Cron developer reference.
  2. Loopback requests are blocked. The queue still moves, but only at one batch per minute. Check whether the site can make HTTP requests to itself.
  3. A long-running action holds the only concurrency slot. Each minute's tick sees the limit reached and exits. The queue looks frozen while one action is in fact still executing.

/ Manual runs

How do you trigger a queue pass by hand?

Three ways, in increasing order of control. In PHP, fire the hook directly — this runs a pass synchronously in the current request:

PHP — force one queue pass

do_action( 'action_scheduler_run_queue', 'Manual' );

From the command line, Action Scheduler ships its own WP-CLI command, which is the right tool for clearing a backlog because it bypasses both the web request time limit and the concurrency guard:

Shell — WP-CLI

# Run batches until the queue is empty (--batches=0 means "keep going").
wp action-scheduler run --batches=0

# Larger batches, only one hook, ignoring the concurrency limit.
wp action-scheduler run --batch-size=200 --hooks=my_plugin_sync_order --force

Note that the WP-CLI command defaults to a batch size of 100, not the 25 used by the web queue runner — it assumes it is running without a web request's constraints. And --force overrides the concurrency limit, which is exactly what you want for a one-off drain and exactly what you do not want on a cron schedule.

For the functions that put actions into the queue in the first place, see the Action Scheduler PHP API reference.

FAQ

Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

What is action_scheduler_run_queue? +
It is the WP-Cron hook Action Scheduler schedules for itself, and the entry point for all queue processing. When WP-Cron fires the hook, ActionScheduler_QueueRunner::run() executes one queue pass: it claims a batch of due actions, runs them, and releases the claim.
How often does action_scheduler_run_queue run? +
It is registered on Action Scheduler own every_minute schedule, a 60-second interval the library adds through the cron_schedules filter, so it fires at most once a minute. Anything faster than that comes from asynchronous loopback requests the runner dispatches when work is still due after a pass.
How do I manually run the Action Scheduler queue? +
In PHP call do_action('action_scheduler_run_queue', 'Manual') to run one pass synchronously. On the command line use wp action-scheduler run --batches=0 to keep processing until the queue is empty, adding --force to bypass the concurrency limit and --batch-size to change the default of 100.
Why is my Action Scheduler queue stuck even though the hook is scheduled? +
A scheduled WP-Cron event only runs when something triggers WP-Cron. The three usual causes are DISABLE_WP_CRON set with no replacement system cron, blocked loopback requests limiting the queue to one batch per minute, and a long-running action holding the single concurrency slot so every tick exits immediately.
What is the context argument passed to the queue runner? +
A descriptive string identifying what triggered the pass. The scheduled cron event passes WP Cron, loopback passes use Async Request, and command-line runs use WP CLI. It is recorded in the Action Scheduler logs and does not change the work performed.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate