Create and Configure Cron Jobs in Magento 2?

A misconfigured or broken cron job doesn't announce itself cleanly. What you notice instead: product prices that stopped updating yesterday, order confirmation emails sitting in a queue, a search index three days behind. By the time someone reports it, the damage is done.

Cron is infrastructure. Most Magento teams treat it like a feature - set it up once, forget about it. That works fine until it doesn't.

This guide covers how Magento's cron system actually works (not just the happy path), how to install and verify it correctly, how to write a custom cron job from scratch, what Magento 2.4.8 changed, and how to debug the problems that will eventually show up in production. Every command and configuration here comes from Adobe's official documentation - last updated April 2026 - or other primary sources.

How Magento 2 Cron Actually Works?

The Unix cron daemon on your server triggers bin/magento cron:run every minute. That command checks the cron_schedule database table for jobs that are due to run, then processes them.

There's a detail here that catches a lot of developers: you must run the command twice to execute a job for the first time. The first run queues the jobs. The second run - which needs to happen on or after the scheduled_at time - executes them. Adobe's docs are explicit about this: "You must run cron twice: the first time to discover tasks to run and the second time - to run the tasks themselves." (Adobe Experience League)

The cron_schedule table tracks every job. The columns that matter for debugging:

Column

What it tells you

job_code

Unique identifier - matches the name attribute in crontab.xml

status

pending / running / success / error / missed

scheduled_at

When the job was supposed to start

executed_at

When it actually started

finished_at

When it completed (null if still running or crashed)

messages

Error output - check this first when status is error

Job statuses have specific meanings. missed means the job didn't start within the schedule_lifetime window - not that it failed, just that it started too late to count. running with a null finished_at and a timestamp from two hours ago means the PHP process died mid-execution. These are different problems with different fixes.

Cron log location (moved from system.log in an earlier version):

<install_directory>/var/log/cron.log

 

Adobe Cloud note: On Adobe Commerce Cloud, cron runs automatically. You configure it in .magento.app.yaml - not via crontab. The setup:cron:run command is not available on Cloud environments.

The Three Cron Groups

Magento organizes cron jobs into three groups. Each group runs in its own process and can have independent timing settings.

Group

Purpose

What runs here

default

General maintenance

Email sending, currency rate updates, sitemap generation, catalog price rules, cache cleanup, sales reports

index

Reindexing

Catalog index, price index, search index - all triggered by MView changelog tables

consumers

Message queue

Async order processing, bulk API operations, inventory reservations, mass admin actions

The separation matters for performance. Index rebuilds are resource-heavy. If they ran in the same group as email delivery, a slow reindex could block confirmation emails from sending. Keeping them isolated means a stalled index job doesn't cascade into other problems.

Run a single group manually:

bin/magento cron:run --group=index

One thing most documentation gets wrong: being in the same cron group does not mean jobs run serially. Magento locks individual jobs, not groups. If a job in a group lags past one minute, the next cron:run spawns a new PHP process and executes other scheduled jobs from the same group in parallel. A real production log from run_as_root on DEV.to (2025) shows exactly this - a job that took 174 seconds allowed other group members to start and complete during its run. Design your jobs with this in mind.

Installing Cron: The Right Way

Log into your server as the Magento filesystem owner, navigate to the Magento root, and run:

bin/magento cron:install

This writes the crontab entry between #~ MAGENTO START and #~ MAGENTO END markers. Use --force to overwrite an existing entry.

Verify with:

crontab -l

The current crontab output (Magento 2.4.0+) looks like this:

#~ MAGENTO START c5f9e5ed71cceaabc4d4fd9b3e827a2b

* * * * * /usr/bin/php /var/www/html/magento2/bin/magento cron:run 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/magento2/var/log/magento.cron.log

#~ MAGENTO END c5f9e5ed71cceaabc4d4fd9b3e827a2b

 

If you find three crontab lines referencing update/cron.php and bin/magento setup:cron:run, remove them. Both were deprecated and removed in Magento 2.4.0. Any guide still showing three lines is outdated. Similarly, dev/tools/cron.sh no longer exists - don't reference it.

On multi-node setups: run the crontab on exactly one node. Multiple nodes executing cron:run simultaneously causes duplicate job processing and database lock conflicts. Use a dedicated cron node or a process manager to enforce single execution (MGT Commerce, 2026).

Admin Configuration

Path: Stores > Configuration > Advanced > System > Cron (Scheduled Tasks)

Per-group settings and what they control:

Setting

Default

What it controls

Generate Schedules Every

1 min

How often new entries are written to cron_schedule

Schedule Ahead For

4 min

How far into the future to pre-generate job entries

Missed if Not Run Within

2 min

Window a job must start in or be marked 'missed'

History Cleanup Every

10 min

How often old entries are removed from the table

Success History Lifetime

60 min

How long to keep records of successful jobs

Failure History Lifetime

600 min

How long to keep records of failed jobs

Use Separate Process

Yes (index)

Run group in isolated PHP process

Practical guidance: keep history_success_lifetime at 60 minutes or less for most stores - you don't need weeks of successful job records in the database. Keep history_failure_lifetime at 600 minutes (10 hours) so you have enough time to catch overnight failures in the morning. If cron_schedule is bloating, these two settings are the first place to look.

If jobs are showing up as missed frequently but the server is healthy, check Missed if Not Run Within. A window of 2 minutes is tight on busy servers. Raising it to 5 minutes is often the right call for high-traffic production environments.

How to Create a Custom Cron Job in Magento 2: Step by Step

This walks through creating a custom cron job from scratch in your own module. The code follows Adobe's official tutorial (last updated April 2026) and Mage-OS DevDocs.

Step 1: Create or confirm your module

You need a registered, enabled module. If you're building one from scratch, see How to Create a Magento 2 Module →. If you have one, confirm it's active:

bin/magento module:status Vendor_ModuleName

Step 2: Create the cron PHP class

Create app/code/Vendor/ModuleName/Cron/YourCronClass.php:

<?php

namespace Vendor\ModuleName\Cron;

 

use Psr\Log\LoggerInterface;

 

class YourCronClass

{

    protected $logger;

 

    public function __construct(LoggerInterface $logger)

    {

        $this->logger = $logger;

    }

 

    public function execute()

    {

        $this->logger->info('[CRON][START]: YourCronClass');

        try {

            // your logic here

            $this->logger->info('[CRON][END]: YourCronClass');

        } catch (\Exception $e) {

            $this->logger->error('[CRON][ERROR]: ' . $e->getMessage());

        }

        return $this;

    }

}

Three things to note: always log start, end, and exceptions - these become your only debugging tool when a job fails silently at 3am. Catch exceptions inside execute() rather than letting them bubble up - an uncaught exception marks the job as error and stops the stack trace from surfacing clearly. Return $this - it keeps method chaining intact and follows Magento conventions (Mage-OS DevDocs).

Step 3: Create crontab.xml

Create app/code/Vendor/ModuleName/etc/crontab.xml:

<?xml version="1.0"?>

<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_modulename_your_cron"

             instance="Vendor\ModuleName\Cron\YourCronClass"

             method="execute">

            <schedule>0 2 * * *</schedule>

        </job>

    </group>

</config>

Attribute breakdown:

  • name - must be globally unique across all modules. Use a vendor-prefixed pattern like vendor_module_jobname to avoid collisions with core jobs or other extensions.
  • instance - the fully-qualified classpath of your cron PHP class.
  • method - the method to call. Always execute by convention.
  • schedule - cron expression. 0 2 * * * means daily at 2:00 AM.

Cron expression format:

Position

1st

2nd

3rd

4th

5th

Field

Minute

Hour

Day of month

Month

Day of week

Range

0–59

0–23

1–31

1–12

0–7 (0,7=Sun)

Example

0

2

*

*

*

Test your expression at crontab.guru before committing.

Step 4: Register and compile

bin/magento setup:upgrade

bin/magento setup:di:compile

bin/magento cache:clean

Step 5: Test the job

Run cron twice - the first triggers scheduling, the second executes:

bin/magento cron:run --group=default

# wait a moment

bin/magento cron:run --group=default

Step 6: Verify in the database

SELECT job_code, status, scheduled_at, executed_at, finished_at, messages

FROM cron_schedule

WHERE job_code LIKE '%your_cron%'

ORDER BY scheduled_at DESC

LIMIT 10;

A success status with a populated finished_at means it worked. If you see error, check the messages column first, then var/log/cron.log.

Custom Cron Groups

Use a custom cron group when your job needs different timing from the default group, or when you want to run a set of jobs in isolation - for example, a resource-intensive export job you don't want competing with email delivery.

Create app/code/Vendor/ModuleName/etc/cron_groups.xml (Adobe reference):

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron_groups.xsd">

    <group id="vendor_custom_group">

        <schedule_generate_every>1</schedule_generate_every>

        <schedule_ahead_for>4</schedule_ahead_for>

        <schedule_lifetime>2</schedule_lifetime>

        <history_cleanup_every>10</history_cleanup_every>

        <history_success_lifetime>60</history_success_lifetime>

        <history_failure_lifetime>600</history_failure_lifetime>

        <use_separate_process>1</use_separate_process>

    </group>

</config>

Then update your crontab.xml to reference the new group:

<group id="vendor_custom_group">

    <job name="vendor_modulename_your_cron" ... >

        <schedule>0 2 * * *</schedule>

    </job>

</group>

Run the group in isolation during development:

bin/magento cron:run --group=vendor_custom_group

 

Remember: same group does not mean serial execution. Individual jobs are locked, not the group. A lagging job will not block other jobs in the same group from starting on the next cron:run. If you need serial execution, implement your own locking mechanism inside the execute() method.

Disabling a Cron Job Without Removing It

There's no disable attribute in Magento's cron XML. The official workaround is to set the schedule to a date that never occurs - February 30:

<!-- Effectively disables the job -->

<job name="some_job" instance="..." method="execute">

    <schedule>0 0 30 2 *</schedule>

</job>

What Changed in Magento 2.4.8?

Magento 2.4.8 was released April 8, 2025 and is supported until April 2028. It introduced four cron-related changes that affect both new stores and upgrades (MGT Commerce, March 2026):

1. Automatic cron_schedule cleanup

The system now removes cron_schedule entries for jobs that no longer exist in the codebase. Previously, uninstalling an extension left orphaned rows that accumulated indefinitely. This was a real problem - at least one production store hit over one million rows in the table, which caused cron execution time to climb steadily.

2. Indexers default to "Update by Schedule"

All new indexers now use schedule mode instead of "Update on Save." This reduces real-time slowdowns when admins save products, but it increases the workload on the index cron group. If your server is under-resourced for cron, this will show up as index jobs running slower or missing their window.

3. Customer Grid indexer now supports both modes

Before 2.4.8, the Customer Grid indexer only supported "Update on Save." It now supports both modes and defaults to "Update by Schedule" (Adobe PHP DevDocs). After upgrading, verify the mode:

bin/magento indexer:show-mode customer_grid

4. Changelog table management

The system now removes unused changelog tables when you switch an indexer from schedule mode back to save mode. Less database clutter.

Monitoring and Debugging

These are the actual commands and queries worth keeping in a runbook.

Check if cron is running at all

# If this file exists, cron has run at least once

ls -al var/log/cron.log

 

# Check current crontab entries

crontab -l

Query job status

-- Recent job status for a specific job

SELECT job_code, status, scheduled_at, executed_at, finished_at, messages

FROM cron_schedule

WHERE job_code = 'your_job_code'

ORDER BY scheduled_at DESC LIMIT 10;

 

-- All failed or missed jobs in the last 24 hours

SELECT job_code, status, messages, scheduled_at

FROM cron_schedule

WHERE status IN ('error', 'missed')

AND scheduled_at > NOW() - INTERVAL 24 HOUR

ORDER BY scheduled_at DESC;

Common problems and fixes

Jobs stuck in 'running' status:

The PHP process died mid-execution. Reset stuck jobs:

UPDATE cron_schedule SET status='error', messages='Process terminated'

WHERE status='running' AND executed_at < NOW() - INTERVAL 1 HOUR;

Jobs consistently showing as 'missed':

Either cron isn't running every minute, or the Missed if Not Run Within window is too tight for your server load. Check both: verify crontab -l shows the minute-interval entry, and consider raising the missed window to 5 minutes in Admin > System > Cron.

cron_schedule table bloat:

On stores running Magento versions before 2.4.8, the table accumulates millions of rows over time. Run during low traffic:

OPTIMIZE TABLE cron_schedule;

Also lower Success History Lifetime to 60 minutes if it's set higher. You don't need days of successful job records in the database.

Log files to check:

  • var/log/cron.log - all cron activity
  • var/log/support_report.log - ERROR and MISSED status jobs
  • var/log/exception.log - ERROR jobs logged as CRITICAL

Cloud vs. On-Premise Configuration

The setup is fundamentally different depending on where Magento is running.

Adobe Commerce Cloud

Don't touch crontab. Cron is configured in .magento.app.yaml (Adobe Cloud Docs). The default config runs php bin/magento cron:run on a spec of * * * * *.

# .magento.app.yaml - default cron config

crons:

    cronrun:

        spec: "* * * * *"

        cmd: "php bin/magento cron:run"

Minimum intervals: 5 minutes for Starter and Pro integration environments. 1 minute for Pro Staging and Production. You can't configure more frequent intervals than these minimums.

To disable cron jobs before maintenance, use the ece-tools CLI:

php ./vendor/bin/ece-tools cron:disable

# Do your maintenance

php ./vendor/bin/ece-tools cron:enable

On-Premise / Managed Hosting

Use bin/magento cron:install as described in Section 3. If you're on managed hosting (Nexcess, Cloudways, etc.), check whether the host pre-configures cron - many do. Run crontab -l to verify before adding your own entry. If your host has pre-configured cron and you add a second entry, you get duplicate execution.

Security

Two things worth knowing that most cron guides skip.

Third-party extension cron jobs run with full Magento framework access. When you install an extension, its registered cron jobs execute with the same privileges as Magento's own jobs. If an extension is compromised or abandoned, its cron job is still running every minute. Audit your cron_schedule table periodically for unexpected job codes. Remove unused extensions - not just disable them - to eliminate their cron registrations.

pub/cron.php creates unnecessary attack surface. Running cron via the web-accessible pub/cron.php is not recommended in production. A September 2025 GitHub issue against 2.4.8 flags this file as a security concern - in modern deployments, cron.php is not needed. Use bin/magento cron:run via the server crontab instead. If cron.php must be accessible, restrict it using .htaccess authentication (see Adobe's secure cron.php guide).

On the broader security picture: CVE-2025-54236 (SessionReaper, CVSS 9.1) was unpatched on 62% of stores six weeks after its September 2025 disclosure (Sansec data via MGT Commerce). The fix is in Adobe Commerce 2.4.8-p3 and later. If you're below that - patch now.

Final Notes

Cron in Magento is not something you configure once and move on from. The cron_schedule table grows. Extension jobs accumulate. Schedules drift when servers get busy. The stores that have cron-related incidents are almost always the ones where nobody checked.

A few habits that prevent most of the problems covered here: verify crontab -l after every deployment. Add cron_schedule status queries to your monitoring. Keep history_success_lifetime low. Remove extensions you don't use. And after upgrading to 2.4.8, run bin/magento indexer:show-mode to confirm your indexers are in the mode you expect.

Checkout our Magento 2 Cron Scheduler Extension.