Items are sitting on the queue — now something has to actually take them off and do the work. That something is a QueueWorker plugin: a small, focused class whose only job is "given one item, process it." Drupal core's cron system automatically discovers every QueueWorker plugin in every enabled module and calls it to drain its queue on every cron run.
What you'll learn in this lesson
- The
@QueueWorkerannotation and what it registers - The one method every worker must implement:
processItem() - Dependency injection in a plugin via
ContainerFactoryPluginInterface - Sharing logic between multiple related workers with a base class
A real QueueWorker plugin
This is the actual worker plugin from the Examples module's cron_example:
namespace Drupal\cron_example\Plugin\QueueWorker;
/**
* A report worker.
*
* @QueueWorker(
* id = "cron_example_queue_1",
* title = @Translation("First worker in cron_example"),
* cron = {"time" = 1}
* )
*/
class ReportWorkerOne extends ReportWorkerBase {
/**
* {@inheritdoc}
*/
public function processItem($data) {
$this->reportWork(1, $data);
}
}
Three things register this worker with Drupal, all inside the annotation: id is the queue name this worker drains — it must exactly match whatever name you used in $queue_factory->get('cron_example_queue_1') when you enqueued items. title is a human-readable label shown on admin pages that list queues. cron tells Drupal's cron system how long, in seconds, to spend processing this specific queue on each cron run — here, just 1 second, intentionally small so a slow worker never dominates the whole cron run.
processItem() — the only method that matters
public function processItem($data) {
$this->reportWork(1, $data);
}
This is the entire contract: processItem($data) receives exactly whatever you passed to createItem() back when the item was queued, fully unserialized and ready to use. If it returns normally, Drupal deletes the item from the queue — it's done. If it throws an exception (including a \Drupal\Core\Queue\SuspendQueueException to deliberately pause processing this queue for the rest of the current cron run), the item is not deleted, and a future cron run will hand it back to processItem() again.
Injecting services into a plugin
abstract class ReportWorkerBase extends QueueWorkerBase implements ContainerFactoryPluginInterface {
public function __construct(array $configuration, $plugin_id, $plugin_definition, StateInterface $state, LoggerChannelFactoryInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->state = $state;
$this->logger = $logger;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$form = new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('state'),
$container->get('logger.factory')
);
return $form;
}
}
Plugins get dependency injection through ContainerFactoryPluginInterface's create() method — a different signature from a form or controller's create(), since it also receives the plugin's own $configuration, $plugin_id, and $plugin_definition alongside the container. Every argument after those three is a service you're pulling in yourself — here, state and logger.factory.
Sharing logic across multiple workers with a base class
Notice ReportWorkerOne extends ReportWorkerBase, not QueueWorkerBase directly. This is a genuinely useful pattern once you have more than one related queue: put the shared constructor, injected services, and any common processing logic (reportWork() here) on an abstract base class, and let each concrete worker's processItem() stay a one-liner that just calls into the shared logic with its own identifying details.
id in @QueueWorker and the name you pass to $queue_factory->get() are matched as plain strings — a typo in either place means your worker silently never runs, with no error, because Drupal simply never associates the two. If items are piling up and processItem() never seems to fire, check this first.Quick check: your worker's
processItem()throws an uncaught exception halfway through. What happens to that queue item on the next cron run? If you said "it gets handed toprocessItem()again" — correct. Drupal only removes an item from the queue afterprocessItem()returns without throwing, so a failed item stays queued and gets retried, potentially forever if the same input keeps causing the same failure.
Key takeaways
@QueueWorker(id, title, cron)registers a plugin against a queue name — theidmust match the name used atcreateItem()time exactly.processItem($data)is the only method you must implement; returning normally deletes the item, throwing leaves it for retry.- Plugins inject services via
ContainerFactoryPluginInterface::create(), whose signature includes the plugin's own configuration/ID/definition alongside the container. - An abstract base class is the right place to share logic and injected services across multiple related workers.
Coming up next
Your worker exists and can process one item — but nothing calls it yet outside of manually running cron. Next: how the cron key in the annotation actually controls when and how much of your queue gets processed on a real cron run.