
Magento 2 cron jobs stop running for a handful of repeatable reasons: the system crontab entry is missing, the PHP binary in it is wrong, a job overran its schedule lifetime and got marked missed, or the cron_schedule table filled with stale rows. Diagnose by reading cron_schedule, then fix the group config that owns the failing jobs.
Cron is the heartbeat of a Magento and Adobe Commerce store. Reindexing on schedule, sending transactional email, generating sitemaps, cleaning sessions, and running the message queue all depend on it. When cron falls behind, the store does not throw an error. It just quietly stops doing the background work, and the first sign is usually stale prices, unsent emails, or a dashboard that feels slow. This guide explains how cron groups are defined, how the scheduler fills and drains the cron_schedule table, and the concrete causes behind jobs that silently stop.
What a cron group actually is
Magento does not run every scheduled job in one undifferentiated queue. Jobs are organized into groups, and each group has its own schedule generation, retention, and process settings. Most core modules use the default group, and indexing uses the index group. A group is a logical bundle that lets you run and tune a set of jobs independently of the rest.
Two XML files in a module’s etc directory define the picture:
crontab.xmldeclares the jobs: which class and method run, on what schedule, inside which group.cron_groups.xmldeclares the group’s behavior: how often schedules are generated, how far ahead, how long before a job counts as missed, and whether the group runs in its own PHP process.
crontab.xml: declaring jobs
A job declaration looks like this:
<?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_module_sync_inventory"
instance="Vendor\Module\Cron\SyncInventory"
method="execute">
<schedule>*/15 * * * *</schedule>
</job>
</group>
</config>
The group id names the group, the job name is a unique identifier, instance is the class to instantiate, and method is the method to call. The <schedule> is standard cron syntax. You omit <schedule> only when the timing is stored in configuration or the database instead. Per Adobe’s custom cron reference, the group id does not have to be unique across modules, so several modules can contribute jobs to the same group.
cron_groups.xml: tuning the group
Creating a custom group is optional. The reason to create one is to run a set of jobs on their own cadence and in their own process, so a heavy job does not starve the rest of the schedule:
<?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_heavy">
<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>
Each field maps to an admin setting under Stores > Configuration > Advanced > System > Cron, shown per group:
| cron_groups.xml field | Admin label | What it controls |
|---|---|---|
| schedule_generate_every | Generate Schedules Every | Minutes between writing new rows to cron_schedule |
| schedule_ahead_for | Schedule Ahead for | How many minutes of future jobs to pre-write |
| schedule_lifetime | Missed if Not Run Within | The window a job has to start before it is marked missed |
| history_cleanup_every | History Cleanup Every | How often old history rows are purged |
| history_success_lifetime | Success History Lifetime | How long success rows are kept |
| history_failure_lifetime | Failure History Lifetime | How long failure rows are kept |
| use_separate_process | Use Separate Process | Whether the group runs in its own PHP process |
The values above are the sample values Adobe shows in its reference, not hard defaults for every install, so always read your own store’s admin before assuming a number.
How the cron_schedule table fills and drains
Everything cron does passes through one database table: cron_schedule. Understanding its lifecycle is the fastest way to diagnose cron problems.
On each run, the scheduler does two things. First, it generates rows for upcoming jobs, writing pending entries up to schedule_ahead_for minutes into the future, every schedule_generate_every minutes. Second, it executes any pending row whose scheduled time has arrived. A row moves through these statuses:
- pending waiting to run.
- running currently executing.
- success finished cleanly.
- missed its scheduled time passed by more than
schedule_lifetimebefore it could start, so it was skipped. - error it ran and threw an exception.
Query the table directly when something is wrong:
SELECT job_code, status, COUNT(*)
FROM cron_schedule
GROUP BY job_code, status
ORDER BY COUNT(*) DESC;
A wall of missed rows means the scheduler is generating jobs faster than it can run them, or the lifetime window is too short. Rows stuck in running mean a process crashed or is overlapping. A table with tens of thousands of rows means history cleanup is not keeping up.
For how this interacts with reindexing specifically, see our guide to cron and indexer health on Adobe Commerce, and for the related question of when to index on schedule at all, indexer update on schedule versus update on save.
Running cron, and running one group
The system crontab calls Magento’s dispatcher, which runs all groups by default. You install the crontab entry with:
bin/magento cron:install
This writes the three cron entries the application needs. To run a single group manually, which is the core of debugging, pass --group:
bin/magento cron:run --group=index
Omit --group and every group runs. Adobe’s configure cron jobs reference and the custom cron tutorial walk through the CLI and the group setup end to end.
Why jobs stop: the real causes
When cron goes quiet, work through these in order.
- The crontab entry is missing or points at the wrong PHP. Run
crontab -las the web user. If there is no Magento entry, nothing fires at all. If the PHP path in the entry differs from your CLI PHP (a common issue after a server PHP upgrade), jobs run under the wrong binary and fail. - schedule_lifetime is too short for a busy server. On a loaded box, cron may not reach a job inside the lifetime window, so it is marked missed and skipped. Raising the group’s Missed if Not Run Within value can clear a recurring backlog of missed jobs.
- A long job blocks the group. If one job in a group runs for minutes, it can starve the others on the same single-process run. Move heavy work to its own group with
use_separate_processset to 1. - Stuck running rows. A crashed PHP process leaves a row in
runningforever, and the scheduler can refuse to re-run that job. Clear stale running rows and investigate why the process died (memory limits are the usual cause). - History cleanup not keeping up. A bloated cron_schedule table slows every cron run. Confirm history lifetimes are sane and that cleanup is actually running.
A useful reset when the table is a mess: truncate cron_schedule, then run bin/magento cron:run twice (once to generate, once to execute) and watch the statuses. If jobs start succeeding, the backlog was the problem, not the jobs.
Cron groups versus message queue consumers
Teams often confuse cron with the message queue. They are different systems with different failure modes.
| Cron groups | Message queue consumers | |
|---|---|---|
| Triggered by | Time schedule in cron_schedule | Messages on a queue (db or RabbitMQ) |
| Runs via | bin/magento cron:run | Consumers, often under Supervisor |
| Typical jobs | Reindex, email, sitemaps, cleanup | Bulk operations, async API, export |
| Fails when | crontab missing, jobs missed | Consumers die, queue backs up |
If your symptom is bulk actions or async API work piling up rather than scheduled jobs, the issue is likely the consumers, covered in keeping RabbitMQ and db workers alive. And if cron is healthy but pages are cold, see full page cache warming.
FAQ
How do I run just one cron group for testing?
Use bin/magento cron:run --group=<group_id>. For example, bin/magento cron:run --group=index runs only the indexing group. Running without --group executes every group, which makes it hard to isolate which group is failing.
What does a job status of “missed” mean?
The job’s scheduled time passed by more than the group’s schedule_lifetime (the Missed if Not Run Within setting) before cron could start it, so the scheduler skipped it. A few missed rows are normal under load. A constant stream of them means the lifetime is too short, the server is overloaded, or a long job is blocking the group.
Should I put heavy custom jobs in the default group?
No. A long-running job in the default group can delay or starve the other jobs that share that single run. Create a custom group in cron_groups.xml, set use_separate_process to 1, and give it its own schedule so it runs independently.
Where are the cron settings in the admin?
Stores > Configuration > Advanced > System > Cron. Each group appears as its own section with fields for schedule generation, the missed window, history retention, and separate process. The values there come from cron_groups.xml defaults unless overridden.
Is it safe to truncate the cron_schedule table?
Yes, as a recovery step. The table only holds scheduling state, not business data. Truncating it clears a backlog of missed, stuck, and stale rows. After truncating, run cron once to regenerate schedules and again to execute them.
Keeping the heartbeat steady
Cron problems are rarely mysterious once you read the cron_schedule table. The jobs are declared in crontab.xml, the behavior is tuned in cron_groups.xml, and the symptoms (missed, stuck, or bloated) each point at a specific fix. Isolating heavy work into its own group with a separate process is the single highest-impact change for a busy store. If you want a team that runs this playbook on production Adobe Commerce and Hyva builds, learn more about Bemeir, our technology partners, and the work we do across Shopify, BigCommerce, and Shopware alongside the deep Magento operations covered here.





