---
title: "action_scheduler_run_queue: Queue Runner Hook Docs"
description: "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."
url: "https://wpwebhooks.org/blog/action-scheduler-run-queue-hook/"
date: "2026-07-26"
---

# action_scheduler_run_queue: Queue Runner Hook Docs

**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](https://actionscheduler.org/) runs on a timer of its own: the library leans on [WP-Cron](https://developer.wordpress.org/plugins/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?".

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()`](https://developer.wordpress.org/reference/functions/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](https://wpwebhooks.org/blog/action-scheduler-concurrent-batches-filter/).

/ Limits

## Which **filters** change what a pass does?

Four, and their defaults are documented in the [Action Scheduler performance guide](https://actionscheduler.org/perf/):

| 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](https://wpwebhooks.org/blog/action-scheduler-database-tables/) 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](https://wpwebhooks.org/blog/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](https://actionscheduler.org/wp-cli/) 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](https://wpwebhooks.org/blog/action-scheduler-api-functions/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"action_scheduler_run_queue: Queue Runner Hook Docs","description":"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.","datePublished":"2026-07-26","dateModified":"2026-07-26","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-run-queue-hook/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/og_image.jpg","width":1200,"height":630,"caption":"action_scheduler_run_queue: Queue Runner Hook Docs"},"keywords":["action scheduler run queue","action scheduler queue runner","action scheduler cron hook","every minute wordpress cron schedule","action scheduler batch size","action scheduler time limit","wp action scheduler run","action scheduler async loopback"]}

{"@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_run_queue: Queue Runner Hook Docs","item":"https://wpwebhooks.org/blog/action-scheduler-run-queue-hook/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is action_scheduler_run_queue?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"How often does action_scheduler_run_queue run?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"How do I manually run the Action Scheduler queue?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Why is my Action Scheduler queue stuck even though the hook is scheduled?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"What is the context argument passed to the queue runner?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}
```
