Cron
One system crontab entry drives every Magento schedule; crontab.xml declares the jobs and cron groups tune how they run.
<job name="vendor_module_job" instance="Vendor\Module\Cron\JobClass" method="execute"><schedule>*/5 * * * *</schedule></job>Magento schedules its own background work. The operating system runs one command every minute, bin/magento cron:run, and Magento decides which jobs are due. Reindexing, order confirmation emails, newsletters, currency rates and sitemap generation all depend on it. A site with no crontab entry looks healthy for a day, then indexes go stale and emails stop.
Declaring a job
A cron job is a public method on a class, declared in a module file named etc/crontab.xml. Every job sits in a group: a label used to run and tune jobs together. Unless you say otherwise, use default. When cron runs, Magento inserts rows into the cron_schedule table with status pending, runs them, and then records success, missed or error. That table is the first place to look when a job seems not to fire.
Tuning a group
Group settings live in etc/cron_groups.xml and in the admin under Stores > Configuration > Advanced > System > Cron. For the default group Magento ships schedule_generate_every 15, schedule_ahead_for 20, schedule_lifetime 15, and use_separate_process 0.
Making it take effect
After adding or editing crontab.xml, run bin/magento setup:upgrade if the module is new, then bin/magento cache:flush, because the merged cron configuration is cached. Check it with bin/magento cron:run --group=default and a look at cron_schedule. Cron code runs in the crontab area, not frontend.
Example
<!-- app/code/Vendor/Reports/etc/crontab.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="vendor_reports_prune_history"
instance="Vendor\Reports\Cron\PruneHistory"
method="execute">
<schedule>0 3 * * *</schedule>
</job>
</group>
</config>When to use it
- Send a nightly stock file to a supplier without tying up a web request.
- Prune a custom log table every morning at 03:00.
- Move a long-running sync into its own cron group so it cannot delay indexing.
- Work out why order confirmation emails stopped going out on a staging server.
More examples
The class the job calls
The instance and method attributes in crontab.xml point here. Dependencies arrive through the constructor, exactly as in a controller. The cutoff date is bound with a placeholder rather than concatenated into the condition. Nothing registers the class anywhere else: the crontab.xml entry plus bin/magento cache:flush is all it takes for the next cron:run to pick it up.
<?php
// app/code/Vendor/Reports/Cron/PruneHistory.php
declare(strict_types=1);
namespace Vendor\Reports\Cron;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Psr\Log\LoggerInterface;
class PruneHistory
{
public function __construct(
private readonly ResourceConnection $resource,
private readonly DateTime $dateTime,
private readonly LoggerInterface $logger
) {
}
public function execute(): void
{
$connection = $this->resource->getConnection();
$table = $this->resource->getTableName('vendor_reports_history');
$cutoff = $this->dateTime->gmtDate('Y-m-d H:i:s', strtotime('-30 days'));
$deleted = $connection->delete($table, ['created_at < ?' => $cutoff]);
$this->logger->info(sprintf('Pruned %d report rows', $deleted));
}
}The one entry the operating system needs
One line every minute, for all groups. cron:install refuses to overwrite an existing Magento block unless you pass -f. In containers and on managed hosting the entry is often created by the platform instead, so check for it before adding a second one: two parallel cron:run processes on the same install cause duplicated and missed jobs.
# Generate and install the crontab for the current shell user
bin/magento cron:install
# Crontab has been generated and saved
crontab -l
# #~ MAGENTO START <sha256 of the install path>
# * * * * * /usr/bin/php8.4 /var/www/magento/bin/magento cron:run >> /var/www/magento/var/log/magento.cron.log 2>&1
# #~ MAGENTO END <sha256 of the install path>
# Remove it again
bin/magento cron:removeA slow job in its own group
The group id must match the group id used in crontab.xml. use_separate_process makes Magento fork a PHP process per job in this group, so a job that runs for ten minutes cannot hold up the rest of the schedule; a longer schedule_lifetime stops its rows being marked missed. Run bin/magento cache:flush afterwards, and you can then run or exclude this group alone with cron:run --group=vendor_supplier or --exclude-group=vendor_supplier.
<!-- app/code/Vendor/Supplier/etc/cron_groups.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron_groups.xsd">
<group id="vendor_supplier">
<schedule_generate_every>15</schedule_generate_every>
<schedule_ahead_for>20</schedule_ahead_for>
<schedule_lifetime>60</schedule_lifetime>
<history_cleanup_every>10</history_cleanup_every>
<history_success_lifetime>60</history_success_lifetime>
<history_failure_lifetime>4320</history_failure_lifetime>
<use_separate_process>1</use_separate_process>
</group>
</config>
Discussion