VOOZH about

URL: https://docs.contao.org/5.x/dev/framework/cron/

⇱ Cron :: Contao Developer Documentation


Contao Summit 2026 in Leipzig 15th and 16th October

Cron

Contao periodically executes some tasks via its own cron functionality. These include mainly cleanup tasks such as

  • Purge expired comment subscriptions
  • Purge expired registrations
  • Purge expired Opt-In tokens
  • etc.

All cronjobs are registered as services and tagged using the contao.cronjob tag. Thus you can find all cronjobs on your system using the following command:

$ vendor/bin/contao-console debug:container --tag contao.cronjob

since 5.3 Starting also with Contao 5.3 you will find a special contao.cron.supervise_workers cronjob. This cronjob will automatically start worker processes for the asynchronous messaging feature. There is, however, a fallback in case you do not configure a proper contao:cron cronjob (see next section). Then all messages (from the default Contao Managed Edition message queues) will be processed within kernel.terminate of the web process.

Configuring the Cron Job

By default the cron tasks are executed after a response is sent back to the visitor when a request to the Contao site has been made.

since 5.1 Starting with version 5.1 Contao detects whether a real cron job is executed or not and thus disables the front end cron automatically if applicable. However, you can modify this behavior via the following configuration:

# config/config.yamlcontao:cron:web_listener:false

The default value is 'auto'.

Command Line

Executing the cron jobs via the command line is done via the contao:cron command:

$ vendor/bin/contao-console contao:cron

This is also the recommended way of periodically executing Contao’s cron jobs. In a Linux crontab you could use the following instructions for example:

* * * * * /usr/bin/php /path/to/contao/vendor/bin/contao-console contao:cron

You are also able to force the the execution of cron jobs via the --force parameter:

$ vendor/bin/contao-console contao:cron --force

You can also execute just one specific cron job from the command line:

$ vendor/bin/contao-console contao:cron "App\Cron\ExampleCron"

The latter can also be combined with the --force option.

Web URL

In order to trigger the execution of cron jobs via a web URL, a request to the _contao/cron route, e.g. https://example.org/_contao/cron, needs to be made. In a Linux crontab you could use the following instructions for example:

* * * * * wget -q -O /dev/null https://example.org/_contao/cron

Triggering cron jobs via the web URL is conceptually equivalent to a regular website visit. The _contao/cron route exists to avoid the overhead of generating a full HTML response, but execution is web-scoped in terms of the cron framework.

This means that cron jobs restricted to the CLI scope are not triggered via this route. The contao.cron.supervise_workers job, for example, which manages background worker processes for asynchronous messaging, only runs in the CLI scope and is therefore never executed via _contao/cron.

Each registered cron job receives a $scope parameter indicating whether it was triggered via CLI or web. The cron job can use this to decide how to behave, for instance by skipping execution entirely, by running only a subset of its logic, or by always running if it is fast enough for the web context. See the Scope section for details and code examples.

PHP’s request execution time limit applies in the web context, typically 30 seconds. Cron jobs that may exceed this limit must only run in the CLI scope.

For reliable execution of all registered cron jobs, setting up a real cron job via the command line is recommended.

Registering Cron Jobs

Registering custom cron jobs is similar to registering to hooks. There are 3 different ways of registering a cron job. The recommended way is using PHP attributes. Which one you use depends on your setup. For example, if you still need to support PHP 7 you can use annotations.

Generally cron jobs can be registered through the contao.cronjob service tag. The following options are supported for this service tag:

OptionDescription
intervalCan be minutely, hourly, daily, weekly, monthly, yearly or a full CRON expression, like */5 * * * *.
methodWill default to __invoke or onMinutely etc. when a named interval is used. Otherwise a method name has to be defined.

Contao implements PHP attributes with which you can tag your service to be registered as a cron job.

// src/Cron/ExampleCron.php
namespace App\Cron;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
#[AsCronJob('hourly')]
class ExampleCron
{
 public function __invoke(): void
 {
 // Do something …
 }
}

In this case the cron job is executed once per hour. As mentioned before this parameter can also be a full CRON expression, e.g. */5 * * * * for “every 5 minutes”.

Contao also supports its own annotation formats via the Service Annotation Bundle.

// src/Cron/ExampleCron.php
namespace App\Cron;
use Contao\CoreBundle\ServiceAnnotation\CronJob;
/** 
 * @CronJob("hourly")
 */
class ExampleCron
{
 public function __invoke(): void
 {
 // Do something …
 }
}

In this case the cron job is executed once per hour. As mentioned before this parameter can also be a full CRON expression, e.g. */5 * * * * for “every 5 minutes”.

As mentioned before you can manually add the contao.cronjob service tag in your service configuration.

# config/services.yamlservices:App\Cron\ExampleCron:tags:- {name: contao.cronjob, interval:hourly }
// src/Cron/ExampleCron.php
namespace App\Cron;
class ExampleCron
{
 public function __invoke(): void
 {
 // Do something …
 }
}

Only the interval parameter is required. In this case the cron job is executed once per hour. As mentioned before this parameter can also be a full CRON expression, e.g. */5 * * * * for “every 5 minutes”.

Scope

In some cases a cron job might want to know in which “scope” it is executed in - i.e. as part of a front end request or as part of the cron command on the command line interface. The Cron service will pass a scope parameter to the cron job’s method.

// src/Cron/HourlyCron.php
namespace App\Cron;
use Contao\CoreBundle\Cron\Cron;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
use Contao\CoreBundle\Exception\CronExecutionSkippedException;
#[AsCronJob('hourly')]
class HourlyCron
{
 public function __invoke(string $scope): void
 {
 // Skip this cron job in the web scope
 if (Cron::SCOPE_WEB === $scope) {
 throw new CronExecutionSkippedException();
 }
 // …
 }
}

Asynchronous cron jobs

The cron job framework executes jobs synchronously in the order they were tagged (normal service priority tags). This means that if you e.g. have 10 cron jobs, and they all take 20 seconds to run, it will take the framework 200 seconds to complete. For most cron jobs, this is not a problem because they don’t usually run 20 seconds.

However, if you have cron jobs that trigger child processes or are asynchronous in any other way, you would want them to start immediately in parallel without blocking the other cron jobs. You can do this by returning a GuzzleHttp\Promise\PromiseInterface:

// src/Cron/HourlyCron.php
namespace App\Cron;
use Contao\CoreBundle\Cron\Cron;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
use Contao\CoreBundle\Exception\CronExecutionSkippedException;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\PromiseInterface;
#[AsCronJob('hourly')]
class HourlyCron
{
 public function __invoke(string $scope): PromiseInterface
 {
 // Skip this cron job in the web scope
 if (Cron::SCOPE_WEB === $scope) {
 throw new CronExecutionSkippedException();
 }
 return $promise = new Promise(static function () use (&$promise): void {
 // Do something that is asynchronous
 $promise->resolve('Done with asynchronous process.');
 });
 }
}

Because most asynchronous processes are most likely things like a spawned child process using Symfony’s Process component, Contao also provides a utility service for that:

// src/Cron/HourlyCron.php
namespace App\Cron;
use Contao\CoreBundle\Cron\Cron;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
use Contao\CoreBundle\Exception\CronExecutionSkippedException;
use Contao\CoreBundle\Util\ProcessUtil;
use GuzzleHttp\Promise\PromiseInterface;
#[AsCronJob('hourly')]
class HourlyCron
{
 public function __construct(private ProcessUtil $processUtil) {}
 public function __invoke(string $scope): PromiseInterface
 {
 // Skip this cron job in the web scope
 if (Cron::SCOPE_WEB === $scope) {
 throw new CronExecutionSkippedException();
 }
 // Long-running process - probably not "ls" :-)
 $promise = $this->processUtil->createPromise(new Process(['ls']));
 // There's even a helper for another application command, so you don't have to worry about
 // finding the right PHP binary etc.:
 $promise = $this->processUtil->createPromise(
 $this->processUtil->createSymfonyConsoleProcess('app:my-command', '--option-1', 'argument-1')
 );
 return $promise;
 }
}

Testing

Contao keeps track of a cronjob’s last execution in the tl_cron_job table. Thus, if you want to test a cron job even though it has already been executed within its defined interval, you can use the the --force command line option as explained above, e.g.

$ bin/console contao:cron "App\Cron\ExampleCron" --force