Chat on WhatsApp
Magento Development 12 min read

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 2 Cron Jobs: Setup and Why Cron Isn't Running

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_generate cron 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_all cron 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 consumers cron group via queue:consumers:start.
  • Session cleanup and log rotationsession.save cleanup, log archiving, cache flushing jobs.
  • Cache invalidation — full-page cache tag invalidation jobs run on a short schedule so changed products clear FPC promptly.
Key takeaway

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.

  1. Schedule generation — on each minute tick, one cron process reads all crontab.xml definitions across every enabled module, computes upcoming schedule times up to schedule_ahead_for minutes in the future, and writes pending rows to cron_schedule.
  2. Job dispatch — a second process reads cron_schedule rows whose scheduled_at has passed, checks that the job is not already running, marks it running, executes the PHP callback, and updates the status to success or error.

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 groupWhat it runsDefault schedule
defaultReindex (mview), sitemap generation, catalog price rules, currency rates, newsletters, session cleanup, log archiving, cache invalidationEvery 1 min (schedule_generate_every: 1)
indexScheduled (mview) indexers that prefer isolation — stock, inventory reservations, catalog search indexer cleanupEvery 1 min
consumersAsync/bulk message queue consumers — async.operations.all, B2B company operations, bulk product attribute updatesEvery 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_schedule table holds rows 4 minutes into the future.
  • schedule_lifetime — how long (in minutes) a pending job remains valid before being marked missed. Default: 2. If cron was down for 3 minutes and restarts, all jobs scheduled during that gap are immediately missed.
  • history_cleanup_every — how often (in minutes) to clean old history rows. Default: 10.
  • history_success_lifetime — how long (in minutes) to keep success rows. Default: 60.
  • history_failure_lifetime — how long (in minutes) to keep error rows. Default: 600.
1minute between cron runs
4minutes scheduled ahead (schedule_ahead_for)
2minute schedule_lifetime window
3cron groups in Magento core

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:install

That 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 4c60efdc74f87af9a6e9c07ef0d73e61

The exact PHP binary path and Magento root path are substituted automatically from your environment. To verify it was written:

crontab -l | grep MAGENTO

To remove the entries (for example, before a migration):

php bin/magento cron:remove

Running 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.log

Note 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 running for more than ~15 minutes (or your longest expected job duration) is stuck.
  • success — job completed normally. History rows are cleaned up after history_success_lifetime minutes (default 60).
  • missed — the scheduled_at time passed while the job was still pending, beyond the schedule_lifetime window. This means cron was not firing frequently enough or was down entirely during that window.
  • error — the job threw an exception. The messages column contains the exception message. History rows are kept for history_failure_lifetime minutes (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;
Key takeaway

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 -l

If you see no MAGENTO START block, install it:

php bin/magento cron:install
crontab -l  # verify
Key takeaway

No 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 magento

Fix: 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 permission

Fix:

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.flag

Fix:

bin/magento maintenance:disable

5. 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:flush

6. 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:flush

7. 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 cron sidecar container that executes bin/magento cron:run every minute via a Dockerfile CMD crond -f.
  • Add a Kubernetes CronJob resource scheduled at * * * * * that runs bin/magento cron:run against the shared volume.
  • Use the Magento Cloud infrastructure pattern: a dedicated cron worker service defined in .magento.app.yaml under the crons key.

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:flush

Reading 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 -5

A 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

  1. Verify the crontab: crontab -l | grep MAGENTO
  2. Check the PHP binary in the crontab matches the PHP version Magento requires.
  3. Run cron manually: php bin/magento cron:run 2>&1 — any fatal errors appear immediately.
  4. Query cron_schedule: look for pending rows and recent success rows.
  5. Check for stuck running rows older than 15 minutes and reset them.
  6. Verify maintenance mode is off: bin/magento maintenance:status.
  7. Check file permissions on var/ and generated/.
  8. Check var/log/magento.cron.log and var/log/system.log for PHP errors or exceptions.
  9. Verify schedule_ahead_for and schedule_lifetime are non-zero in admin config.
  10. 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