The Queue API — which you also met in the Module Development tutorial — solves a specific performance problem: some work is too slow to do inside a normal request without making a visitor wait, but doesn't need to happen instantly either. Sending a batch of emails, syncing data with a slow external API, resizing a freshly uploaded image into every configured style — all candidates for "do it soon, not right now."
What you'll learn in this lesson
- Why queuing is a performance technique, not just an architectural nicety
- The claim-process-delete cycle and why it protects against partial failures
- How queue processing actually gets triggered in production
Why this is a performance lesson, not just an architecture one
Without a queue, a module that needs to send 500 notification emails after a bulk action would either send all 500 synchronously (the visitor's browser sits there for however long that takes) or risk a timeout partway through. With a queue, the bulk action enqueues 500 items instantly and returns control to the visitor immediately — the actual sending happens incrementally across subsequent cron runs, each processing a bounded number of items per run.
The core pattern
// Adding work — fast, just writes to the queue's own database table.
$queue = \Drupal::queue('mymodule_email_queue');
foreach ($recipient_ids as $uid) {
$queue->createItem(['uid' => $uid, 'template' => 'welcome']);
}
// Processing — typically inside hook_cron(), bounded per run.
function mymodule_cron() {
$queue = \Drupal::queue('mymodule_email_queue');
$end = time() + 15; // Hard time budget for this cron run.
while (time() < $end && $item = $queue->claimItem()) {
try {
_mymodule_send_email($item->data);
$queue->deleteItem($item);
}
catch (\Exception $e) {
// Leave it claimed; it'll automatically become available again
// after the lease expires, and get retried on a future run.
\Drupal::logger('mymodule')->error($e->getMessage());
}
}
}
How processing actually triggers in production
Queue processing normally happens inside hook_cron(), which means it's only as reliable as your cron setup from the previous lesson — a real system cron job running drush cron regularly, not core's opportunistic Automated Cron. For queues needing faster turnaround than your cron interval allows, a dedicated worker process running drush queue:run <queue_name> on its own tighter schedule is the standard pattern.
Quick check: your cron run gets killed by a server restart while an item is claimed but not yet deleted. What happens to that item? If you said it becomes available for claiming again once the lease expires, so it gets retried rather than silently lost, you've understood exactly why claim-then-delete beats a simpler "grab and process" approach.
Key takeaways
- Queuing turns a slow synchronous operation into an instant enqueue plus incremental background processing.
- The claim-then-delete cycle means a crashed cron run retries unfinished items instead of losing them.
- Give each cron-driven processing loop a hard time budget so one queue can't starve every other module's cron work.
- Queue processing is only as reliable as the cron setup underneath it — a real system cron job, not Automated Cron.
Coming up next
You've now got queued work happening reliably. Next, we tune how fast that queue actually drains — optimizing queue worker throughput itself.