Skip to main content
Start of main content.

Automatic message scheduling and replacing hook_cron

by daniel.phin /

Share this post on social media

Symfony Scheduler provides a viable replacement to hook_cron wherein messages can be scheduled for dispatch at a predefined interval. Messages are dispatched the moment they are scheduled, and there is no message duplication, making tasks more reliable and efficient.

This post is part 4 in a series about Symfony Messenger.

  1. Introducing Symfony Messenger integrations with Drupal
  2. Symfony Messenger’ message and message handlers, and comparison with @QueueWorker
  3. Real-time: Symfony Messenger’ Consume command and prioritised messages
  4. Automatic message scheduling and replacing hook_cron
  5. Adding real-time processing to QueueWorker plugins
  6. Making Symfony Mailer asynchronous: integration with Symfony Messenger
  7. Displaying notifications when Symfony Messenger messages are processed
  8. Future of Symfony Messenger in Drupal

With this, the sm worker provided by the SM project, the Symfony Messenger integration with Drupal, can be solely relied on. Rather than legacy runners such as Drupal web cron, request termination cron (automated_cron.module), Drush cron, and Ultimate Cron.

Scheduler functionality is implemented by the Symfony Scheduler component. The Drupal integration is provided by the SM Scheduler module

Schedule provider

Create a message and message handler as usual, then create a Schedule Provider:


declare(strict_types = 1);

namespace Drupal\my_module\Messenger;

use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

final class MyScheduleProvider implements ScheduleProviderInterface {

   * {@inheritdoc}
  public function getSchedule(): Schedule {
    return (new Schedule())->add(
      RecurringMessage::every('5 minutes', new MyMessage()),


A schedule provider is:

  • a class at the Messenger\ namespace
  • with a #[AsScheduler] class attribute
  • implementing \Symfony\Component\Scheduler\ScheduleProviderInterface
  • implements an getSchedule method. This method returns a message instance and the schedule frequency.

For dependency injection, schedule providers have autowiring enabled.

What would normally be the contents of a hook_cron hook would instead be added to the message handler. The message itself does not need to store any meaningful data.

Instead of intervals via RecurringMessage::every(...), crontab syntax can be used:

\Symfony\Component\Scheduler\RecurringMessage::cron('*/5 * * * *', new MyMessage());

Running the worker

Lastly, schedulers must be run via the consume command with a dedicated transport. The transport name is the schedule ID prefixed by scheduler_. For example, given the scheduler ID my_scheduler_name from above, the transport name will be scheduler_my_scheduler_name.

The command finally becomes: sm messenger:consume scheduler_my_scheduler_name .


Messages will be dispatched the moment their interval arrives. Normally intervals begin when the worker is initiated, however you can set a point in time to begin interval computation using the \Symfony\Component\Scheduler\RecurringMessage::every   $from parameter.

The worker must be running at the time when a message is scheduled to be sent. The transport won't retroactively catch-up with messages not dispatched during the time it wasn't running.

The next post outlines how to intercept legacy Drupal @QueueWorker items and insert them into the message bus.