Magento 2 Cron Jobs: Setup and Why Cron Isn't Running
When Magento cron stops firing, your store silently breaks: indexes go stale, transactional emails queue forever, and scheduled prices never apply. This guide covers the exact crontab lines, cron groups, cron_schedule diagnostics, and every root cause fix for Magento 2.4.4 — 2.4.9.
Magento cron is the heartbeat of your store. Every scheduled price rule, transactional email, product reindex, XML sitemap update, and async bulk operation depends on it. When it stops, the store looks fine on the surface — but indexes grow stale, customers stop receiving order confirmation emails, and catalog price rules fire at random. This guide walks you through installing cron correctly on Magento 2.4.4 — 2.4.9, understanding the cron groups and their schedules, reading the cron_schedule table like a pro, and fixing every root cause we have encountered across dozens of production Magento stores.
What Magento cron drives
Before diagnosing a broken cron, it helps to know what depends on it. Cron is not just for sitemap generation — it underpins the entire store's asynchronous layer.
- Reindexing — when indexers are set to "Update by Schedule", mview delta rows pile up until cron processes them. A stopped cron means stale prices and search results.
- Transactional emails — order confirmations, shipment tracking, invoice PDFs. Magento queues them; cron dispatches them. No cron = no emails.
- Scheduled import/export — CSV product feeds, customer export jobs.
- XML sitemap generation — the
sitemap_generatecron job re-crawls the catalog on a configurable schedule (default: daily). - Currency rate updates — fetches exchange rates from external services.
- Catalog price rules and scheduled pricing — start/end dates on special prices use the
catalogrule_apply_allcron job. Stop cron and scheduled prices never apply. - Message queue consumers (async/bulk API) — asynchronous REST operations, bulk product updates, and B2B company operations are dispatched by the
consumerscron group viaqueue:consumers:start. - Session cleanup and log rotation —
session.savecleanup, log archiving, cache flushing jobs. - Cache invalidation — full-page cache tag invalidation jobs run on a short schedule so changed products clear FPC promptly.
If cron is not running, your store is essentially running in read-only mode with increasingly stale data. Emails, prices, indexes, and sitemaps all degrade silently — there is no admin alert by default.
How Magento cron works — the two-step model
Magento cron operates as a two-phase process that runs on a single system crontab entry firing every minute.
- Schedule generation — on each minute tick, one cron process reads all
crontab.xmldefinitions across every enabled module, computes upcoming schedule times up toschedule_ahead_forminutes in the future, and writespendingrows tocron_schedule. - Job dispatch — a second process reads
cron_schedulerows whosescheduled_athas passed, checks that the job is not alreadyrunning, marks itrunning, executes the PHP callback, and updates the status tosuccessorerror.
Both phases are triggered by the same * * * * * crontab line. The key insight is that cron never fires directly from the OS scheduler into a job handler — it fires into Magento's own scheduler which then decides what to run. This is why the system crontab entry is necessary but not sufficient: if the cron_schedule table has no pending rows, nothing runs even if the OS cron fires correctly.
Cron groups and default schedules
Magento 2.4.4 — 2.4.9 ships with three cron groups. Each group runs in its own separate process and can have independent schedule settings. Third-party modules can add their own groups; the three below are Magento core.
| Cron group | What it runs | Default schedule |
|---|---|---|
default | Reindex (mview), sitemap generation, catalog price rules, currency rates, newsletters, session cleanup, log archiving, cache invalidation | Every 1 min (schedule_generate_every: 1) |
index | Scheduled (mview) indexers that prefer isolation — stock, inventory reservations, catalog search indexer cleanup | Every 1 min |
consumers | Async/bulk message queue consumers — async.operations.all, B2B company operations, bulk product attribute updates | Every 1 min; individual consumer runtime capped by max_messages |
Each group's timing parameters are set in etc/cron_groups.xml (module-level) or overridden in the Magento admin at Stores > Configuration > Advanced > System > Cron (Scheduled Tasks). The key parameters are:
- schedule_generate_every — how often (in minutes) to generate new schedule rows. Default: 1.
- schedule_ahead_for — how far ahead (in minutes) to generate rows. Default: 4. This means at any moment, the
cron_scheduletable holds rows 4 minutes into the future. - schedule_lifetime — how long (in minutes) a
pendingjob remains valid before being markedmissed. Default: 2. If cron was down for 3 minutes and restarts, all jobs scheduled during that gap are immediatelymissed. - history_cleanup_every — how often (in minutes) to clean old history rows. Default: 10.
- history_success_lifetime — how long (in minutes) to keep
successrows. Default: 60. - history_failure_lifetime — how long (in minutes) to keep
errorrows. Default: 600.
Installing cron — the correct way
The canonical installation method on Magento 2.4.4 — 2.4.9 is the built-in CLI command. Run it as the web server user (the same user that owns var/ and pub/).
php bin/magento cron:installThat command writes two entries into the current user's crontab — one for the default group and one that triggers all groups. On a standard installation they look like this:
#~ MAGENTO START 4c60efdc74f87af9a6e9c07ef0d73e61
* * * * * /usr/bin/php /var/www/html/bin/magento cron:run 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/var/log/magento.cron.log
* * * * * /usr/bin/php /var/www/html/bin/magento cron:run --group index 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/var/log/magento.cron.log
#~ MAGENTO END 4c60efdc74f87af9a6e9c07ef0d73e61The exact PHP binary path and Magento root path are substituted automatically from your environment. To verify it was written:
crontab -l | grep MAGENTOTo remove the entries (for example, before a migration):
php bin/magento cron:removeRunning cron manually for testing
You can trigger cron jobs immediately without waiting for the OS scheduler:
# Run all groups
php bin/magento cron:run
# Run a specific group only
php bin/magento cron:run --group default
php bin/magento cron:run --group index
php bin/magento cron:run --group consumers
# Run and watch the output
php bin/magento cron:run 2>&1 | tee /tmp/cron-test.logNote that cron:run only dispatches jobs that have pending rows in cron_schedule. If the table is empty (schedule generation has never run), you will see no output. Run it twice — first run generates the schedule, second run dispatches pending jobs.
Writing a custom cron job
If you are building a custom module under app/code/Panth/, declare your cron job in 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="panth_module_sync_feeds" instance="Panth\Module\Cron\SyncFeeds" method="execute">
<schedule>*/15 * * * *</schedule>
</job>
</group>
</config>The instance is the PHP class (must be injectable via the ObjectManager); method is the public method Magento calls. After adding the file, run bin/magento setup:upgrade && bin/magento cache:flush so the cron configuration is rebuilt.
Diagnosing cron with the cron_schedule table
The cron_schedule table is your first and most reliable diagnostic tool. Every cron job that Magento knows about has rows here. Pull the last 30 rows ordered by schedule time:
SELECT job_code, status, scheduled_at, executed_at, finished_at, messages
FROM cron_schedule
ORDER BY scheduled_at DESC
LIMIT 30;Reading the status values
- pending — row created by the schedule generator; the job has not been dispatched yet. Healthy: you should always see pending rows a few minutes in the future.
- running — job is currently executing. A row that stays
runningfor more than ~15 minutes (or your longest expected job duration) is stuck. - success — job completed normally. History rows are cleaned up after
history_success_lifetimeminutes (default 60). - missed — the
scheduled_attime passed while the job was stillpending, beyond theschedule_lifetimewindow. This means cron was not firing frequently enough or was down entirely during that window. - error — the job threw an exception. The
messagescolumn contains the exception message. History rows are kept forhistory_failure_lifetimeminutes (default 600).
Key diagnostic queries
-- Check for error messages
SELECT job_code, messages, scheduled_at
FROM cron_schedule
WHERE status = 'error'
ORDER BY scheduled_at DESC
LIMIT 20;
-- Count by status (snapshot health check)
SELECT status, COUNT(*) AS cnt
FROM cron_schedule
GROUP BY status;
-- Find stuck running jobs
SELECT job_code, executed_at, TIMESTAMPDIFF(MINUTE, executed_at, NOW()) AS running_minutes
FROM cron_schedule
WHERE status = 'running'
ORDER BY executed_at ASC;
-- Find the most recent execution of each job
SELECT job_code, MAX(finished_at) AS last_success
FROM cron_schedule
WHERE status = 'success'
GROUP BY job_code
ORDER BY last_success ASC;If SELECT COUNT(*) FROM cron_schedule WHERE status='pending'; returns zero and there are no recent success rows, cron has never run or the schedule generator is broken. Start by running bin/magento cron:run manually from the CLI and checking whether rows appear.
Why cron isn't running — root causes and fixes
1. No crontab installed
The most common cause by far. If cron:install was never run — or the crontab was wiped during a server migration — the OS never fires cron at all.
Check:
crontab -lIf you see no MAGENTO START block, install it:
php bin/magento cron:install
crontab -l # verifyNo crontab = nothing runs. This is the single most common cause of "magento cron not running" tickets. Always verify the crontab first before any other diagnosis.
2. Wrong PHP binary or path in the crontab
Shared hosting environments and Docker containers frequently have multiple PHP versions. The crontab may reference /usr/bin/php which resolves to PHP 7.4 while Magento 2.4.9 requires PHP 8.2 or 8.3. The cron process silently exits with a PHP version error written only to the cron log — not to magento.cron.log.
Check:
# Which PHP is on your PATH?
which php && php -v
# What does crontab use?
crontab -l | grep magentoFix: Edit the crontab directly and pin the full path:
crontab -e
# Replace /usr/bin/php with /usr/local/php83/bin/php (your actual path)Or re-run bin/magento cron:install after ensuring the correct PHP is first on $PATH.
3. File permission errors
Cron runs as the system user (often www-data or the hosting account user). If var/, generated/, pub/static/, or pub/media/ are not writable by that user, cron fires but every job fails silently or produces permission-denied exceptions in var/log/system.log.
Check:
ls -la var/ generated/ pub/static/
tail -50 var/log/system.log | grep -i permissionFix:
find var generated pub/static pub/media -type d -exec chmod 775 {} \;
find var generated pub/static pub/media -type f -exec chmod 664 {} \;4. Maintenance mode enabled
When Magento is in maintenance mode (var/.maintenance.flag exists), bin/magento cron:run exits immediately without processing any jobs. This is by design — maintenance mode is intended to stop all background processing during deployments.
Check:
bin/magento maintenance:status
# Or directly:
ls var/.maintenance.flagFix:
bin/magento maintenance:disable5. MAGE_MODE not set or set to developer incorrectly
In developer mode, cron runs normally, but bin/magento setup:di:compile is not run, so generated proxies and interceptors may be missing. Jobs that depend on a compiled DI graph throw Class ... not found errors. In production mode without compilation, the same problem occurs.
Fix for production:
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush6. schedule_ahead_for / schedule_lifetime misconfiguration
If schedule_ahead_for is set to 0 or a very small number, no future rows are written and cron appears broken. If schedule_lifetime is set to 0, every pending job is immediately marked missed. Both values can be accidentally reset by importing a config dump.
Check (Magento admin): Stores > Configuration > Advanced > System > Cron (Scheduled Tasks) > Default Group.
Fix via CLI:
bin/magento config:set system/cron/default/schedule_ahead_for 4
bin/magento config:set system/cron/default/schedule_lifetime 2
bin/magento config:set system/cron/index/schedule_ahead_for 4
bin/magento config:set system/cron/index/schedule_lifetime 2
bin/magento cache:flush7. Stuck "running" rows blocking subsequent jobs
Magento uses an optimistic lock based on the status column. If a job process crashes mid-execution (OOM kill, PHP timeout, server restart), the row stays running forever. The scheduler will not queue another instance of that job while a running row exists — so frequently-run jobs appear to stop firing entirely after a single crash.
Check:
SELECT job_code, executed_at, TIMESTAMPDIFF(MINUTE, executed_at, NOW()) AS running_minutes
FROM cron_schedule
WHERE status = 'running'
ORDER BY running_minutes DESC;Fix:
-- Reset jobs stuck running for more than 15 minutes
UPDATE cron_schedule
SET status = 'missed'
WHERE status = 'running'
AND executed_at < NOW() - INTERVAL 15 MINUTE;Then investigate why the job crashed — check var/log/system.log and var/log/exception.log for OOM or timeout messages.
8. Missing setup:upgrade after module install
Custom cron jobs declared in etc/crontab.xml are not picked up until setup:upgrade regenerates the compiled configuration. If you install a module or add a cron job and skip the upgrade step, the job simply does not exist in the runtime — no error, no rows in cron_schedule.
bin/magento setup:upgrade
bin/magento cache:flush
bin/magento cron:run # trigger schedule generation
9. Containerised environments without a cron daemon
Docker and Kubernetes deployments often run a single process per container. The PHP-FPM or nginx container has no cron daemon — cron:install writes to a crontab that is never read. Magento cron then simply never fires.
Fix options:
- Run a dedicated
cronsidecar container that executesbin/magento cron:runevery minute via a DockerfileCMD crond -f. - Add a Kubernetes CronJob resource scheduled at
* * * * *that runsbin/magento cron:runagainst the shared volume. - Use the Magento Cloud infrastructure pattern: a dedicated cron worker service defined in
.magento.app.yamlunder thecronskey.
10. Large flat catalog causing cron timeouts
Stores with flat product or category tables enabled (Stores > Configuration > Catalog > Storefront > Use Flat Catalog) can generate reindex jobs that run for 10–20 minutes per execution. These jobs hold the running lock, starve other jobs, and eventually hit PHP's max_execution_time — leaving a stuck row. On Magento 2.4.4 — 2.4.9, flat catalog is essentially legacy; disable it and switch all indexers to "Update by Schedule".
bin/magento config:set catalog/frontend/flat_catalog_product 0
bin/magento config:set catalog/frontend/flat_catalog_category 0
bin/magento indexer:set-mode schedule
bin/magento indexer:reindex
bin/magento cache:flushReading magento.cron.log
The crontab lines generated by cron:install redirect output to var/log/magento.cron.log. This is your second most important diagnostic source after the database.
# Watch live
tail -f var/log/magento.cron.log
# Find errors in the last 1000 lines
tail -1000 var/log/magento.cron.log | grep -i error
# Check when cron last fired (any successful run prints a timestamp)
grep "Ran jobs by schedule" var/log/magento.cron.log | tail -5A healthy cron log shows lines like [2026-05-23 10:45:00] Ran jobs by schedule: default every minute. Complete silence for more than 2 minutes means the OS crontab is not firing. PHP errors (wrong binary, missing extensions) appear here and nowhere else.
Complete diagnosis checklist
- Verify the crontab:
crontab -l | grep MAGENTO - Check the PHP binary in the crontab matches the PHP version Magento requires.
- Run cron manually:
php bin/magento cron:run 2>&1— any fatal errors appear immediately. - Query
cron_schedule: look forpendingrows and recentsuccessrows. - Check for stuck
runningrows older than 15 minutes and reset them. - Verify maintenance mode is off:
bin/magento maintenance:status. - Check file permissions on
var/andgenerated/. - Check
var/log/magento.cron.logandvar/log/system.logfor PHP errors or exceptions. - Verify
schedule_ahead_forandschedule_lifetimeare non-zero in admin config. - In containers: confirm a cron daemon is running in the cron service container.
Most "magento cron not running" issues resolve at step 1 or step 2. The remaining steps cover the edge cases that survive initial triage.
For related performance context — including why a slow TTFB compounds the impact of stale cron-driven indexes — see the post on Magento TTFB optimization from 1.8 s to 180 ms. If you are upgrading to Magento 2.4.9 and seeing new cron behavior (the 2.4.8 auto-cleanup changes, new indexers set to schedule by default), the Magento 2.4.9 upgrade guide covers what changed.
FAQ
How do I know if cron is actually running on my server?
Three quick checks: crontab -l | grep MAGENTO (crontab exists), tail -5 var/log/magento.cron.log (recent activity), and SELECT COUNT(*) FROM cron_schedule WHERE status='success' AND finished_at > NOW() - INTERVAL 5 MINUTE; (recent successes in the database). If all three show activity, cron is running.
Why are all my cron jobs showing status 'missed'?
Missed means the job's scheduled_at time passed while it was still pending, beyond the schedule_lifetime window (default 2 minutes). This happens when cron was not firing at all, when it was firing very slowly (wrong PHP binary causing slow startup), or when schedule_lifetime was set to 0. Fix the underlying cause and the missed status clears naturally as new rows are generated.
Can I run cron:run from a browser or web request?
No, and you should not try. bin/magento cron:run is a CLI command that expects shell environment variables and a TTY-compatible SAPI. Running it via a web request (for example, a cURL hit to a PHP wrapper script) produces unpredictable behavior and is a security risk. Always trigger it from the OS crontab or a dedicated CLI container.
How do I add a cron job that runs in my custom Panth module?
Create app/code/Panth/YourModule/etc/crontab.xml, declare a job pointing to your PHP class and method, run bin/magento setup:upgrade && bin/magento cache:flush, and verify the job appears in cron_schedule after the next cron:run. Use the default group unless your job is resource-intensive, in which case create a custom group in etc/cron_groups.xml with its own use_separate_process flag.
Does Magento 2.4.8 or 2.4.9 change anything about cron?
Yes. Magento 2.4.8 introduced automatic cron_schedule table cleanup and changed all new indexers to "Update by Schedule" mode by default. If you upgrade from 2.4.6 or earlier, you may notice the cron_schedule table is smaller after upgrade and that more indexers are running on schedule instead of requiring a manual reindex. The two-crontab-line setup from cron:install is unchanged.
Cron still misbehaving? Persistent cron failures often point to a deeper infrastructure issue — wrong PHP-FPM configuration, a containerisation gap, or a module with a blocking job that never finishes. I audit Magento installations on Magento 2.4.4 — 2.4.9, pinpoint the root cause, and ship a fix the same sprint. Fixed-fee options start at a $499 audit. See services or go straight to hire me to scope your issue.
Get a Magento developer on it