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_minuteschedule (a 60-second interval it adds itself). - When the hook fires,
ActionScheduler_QueueRunner::run()executes with the context stringWP 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?".
/ 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:
| Filter | Default | What it controls |
|---|---|---|
action_scheduler_queue_runner_batch_size | 25 | How many actions a single claim locks in one batch. |
action_scheduler_queue_runner_concurrent_batches | 1 | How many batches may run at the same time across all runners. |
action_scheduler_queue_runner_time_limit | 30 | Seconds one pass may spend executing before it stops claiming more work. |
action_scheduler_run_queue | — | Not 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:
DISABLE_WP_CRONis true with no replacement. Defining the constant switches off the page-load trigger. If nothing callswp-cron.phpon a schedule after that,action_scheduler_run_queueis scheduled forever and fired never. The fix is a real system cron entry, covered in the WP-Cron developer reference.- 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.
- 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.