Configuring queues for cron executionfor Drupal 11 , 10 , 9 , and 8

Last updated :  

You've written a QueueWorker with a working processItem() — but nothing calls it unless you trigger cron yourself. This lesson closes the loop: exactly how the cron key in @QueueWorker controls when, and for how long, your queue actually gets drained during a real cron run.

What you'll learn in this lesson

  • The cron = {"time" => N} annotation key and what it actually limits
  • What happens when cron runs out of time mid-queue
  • Multiple workers on separate queues running independently in the same cron cycle
  • Triggering cron manually versus relying on Drupal's automated cron

The cron annotation key, revisited

/**
 * @QueueWorker(
 *   id = "cron_example_queue_1",
 *   title = @Translation("First worker in cron_example"),
 *   cron = {"time" = 1}
 * )
 */
class ReportWorkerOne extends ReportWorkerBase { /* ... */ }

cron = {"time" = 1} is a time budget, in seconds, not an item count. On every cron run, Drupal's queue-processing logic calls your worker's processItem() repeatedly, in a loop, checking the elapsed time after each call — and stops as soon as it's spent roughly that many seconds on this specific queue, even if items remain. Those remaining items simply wait for the next cron run.

This is exactly why the example uses a tiny 1-second budget: it's a deliberate design choice so this one small demo worker can never dominate a real site's cron run and starve every other module's queues and cron hooks of processing time.

Multiple workers, independent budgets

The same module ships a second, near-identical worker:

/**
 * @QueueWorker(
 *   id = "cron_example_queue_2",
 *   title = @Translation("Second worker in cron_example"),
 *   cron = {"time" = 1}
 * )
 */
class ReportWorkerTwo extends ReportWorkerBase {

  public function processItem($data) {
    $this->reportWork(2, $data);
  }

}

Each @QueueWorker plugin gets its own independent time budget — a slow queue running near its limit doesn't eat into a different queue's allowance. Drupal's cron loop simply walks through every registered QueueWorker plugin, one after another, giving each its own slice.

Triggering cron: automatic versus manual

In production, cron normally runs one of three ways: core's Automated Cron module firing opportunistically at the end of a page request once enough time has elapsed since the last run, an external system cron job hitting /cron/{key} on a schedule, or an administrator clicking "Run cron manually" at /admin/config/system/cron. All three paths converge on exactly the same queue-processing logic — there's no separate code path for "queues triggered by real cron" versus "queues triggered by the manual button."

public function submitRunCron(array &$form, FormStateInterface $form_state) {
  $this->cron->run();
  // ...
}

This is genuinely all it takes to trigger cron programmatically too — the injected cron service's run() method, exactly as queue_example's own form does for its "Run cron manually" button. Handy during development when you don't want to wait for automated cron's opportunistic timing.

A tiny time budget can mean a queue never fully drains under load: if items arrive faster than a 1-second-per-run budget can process them, the queue backlog grows indefinitely even though everything is technically "working." Size your cron time budget to your queue's realistic throughput needs — a demo module can afford 1 second; a production notification queue processing thousands of items an hour usually needs a much larger budget, or more frequent cron runs.

Quick check: your site's automated cron runs every 3 hours, and your QueueWorker has a 5-second time budget. Items are added to the queue at a slow, steady trickle. Is a 5-second budget, run every 3 hours, actually fine here? If you said yes — the two settings aren't really coupled: what matters is whether 5 seconds' worth of processing, however often it runs, keeps up with however fast items actually arrive. A slow trickle needs far less budget than a bursty flood.

Key takeaways

  • The cron annotation key is a per-run time budget in seconds, not an item-count limit.
  • Each QueueWorker plugin gets its own independent time budget on every cron run — they don't compete with each other.
  • Automated cron, an external system cron hitting /cron/{key}, and the manual "Run cron" button all trigger the exact same queue-processing logic.
  • Size your time budget against realistic arrival rate, not an arbitrary small number — a backlog that never drains is a real production problem, not a cosmetic one.

Coming up next

You've now covered the full queue lifecycle: create, enqueue, process, and schedule via cron. Next, we shift from background processing to something users see and interact with directly in the browser — implementing AJAX callbacks.