crontab -l under a login nobody remembers creating. That is where the real business logic of one server lived: a nightly cleanup, an export, a retry script.

Symfony 6.3 came out yesterday with a Scheduler component. Periodic tasks defined in PHP, executed as Messenger messages. Experimental, but the idea deserves a note.

#[AsSchedule('default')]
final class MainSchedule implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return (new Schedule())->add(
            RecurringMessage::every('10 minutes', new CleanupExpiredCarts()),
        );
    }
}

Then messenger:consume scheduler_default, and a worker fires the messages.

What this gives over crontab. The task is a message, so it goes through the same middleware as everything else: retries, failure transport, logging. Crontab gives you an email nobody reads. And the schedule lives in the repository, so it travels through code review instead of through someone’s shell history.

Two things to get right. Several workers, add the lock, or two of them fire the same task. And decide what happens to runs missed while the worker was down. The component can store state and catch up, but think first whether you want a cleanup to fire forty times after a long deploy. Usually you want it once.

Where I still take plain cron: one server, one or two tasks, no queue in the project. */10 * * * * calling a console command is honest and everyone understands it. Bringing up Messenger workers under supervisor to run a nightly cleanup is architecture for the sake of architecture.

But if the workers are already there, the schedule might as well live with the code. I am moving that zoo of cron entries over, slowly. The old login stays for now. Nobody knows what else it owns.