What is Magento Cron?
Short answer
Magento cron is the system that runs Magento’s scheduled background jobs. One line in the server crontab calls bin/magento cron:run every minute. Each module lists its jobs in crontab.xml, Magento queues them in the cron_schedule table, and they run in groups such as default, index and consumers. Indexers, async order emails, scheduled price changes, sitemap generation and currency rate updates all depend on it.
When cron stops, nothing errors on screen. Jobs just stop running, and you find out days later from stale prices or missing emails. Below I explain how the pieces fit, how I set cron up on each kind of environment, and the four mistakes I see most.
- cron:install adds the host crontab entry 01
- crontab.xml registers tasks per module 02
- cron_schedule table is the run journal 03
- Each group has its own schedule and history settings 04
- Consumer jobs run via queue:consumers:start 05
- Cron groups 3 (default · index · consumers)
- Run interval Every minute (system cron)
- Failure mode Silent: jobs just stop running
Most common mistake Forgetting cron:install after a server migration
The five moving parts of Magento cron
Cron has five parts: the server crontab, each module's crontab.xml, the cron_schedule table, per-group settings and the queue consumers. Once you know where each one lives, debugging cron means checking them in order.
-
cron:install adds the host crontab entry
Running
bin/magento cron:installas the user that owns the code adds one block to that user’s crontab: a line that runsbin/magento cron:runevery minute and appends the output tovar/log/magento.cron.log. I check it withcrontab -l. The server’s cron daemon is the real scheduler; Magento only decides which jobs are due each time it is called. -
crontab.xml registers tasks per module
Each module declares its jobs in
etc/crontab.xml: a job name, a PHP class and method, a group, and either a cron expression or a config path that holds one. Magento merges every module’s file into one job list, andcron:runreads it each minute to decide what is due. -
cron_schedule table is the run journal
Each scheduled run gets a row in the
cron_scheduletable, and its status moves frompendingtorunningtosuccess,missedorerror. Amissedrow means the job didn’t start within the “Missed if Not Run Within” window. Anerrorrow keeps the exception text in themessagescolumn. When I audit a store, my first query counts the error and missed rows from the last day. -
Each group has its own schedule and history settings
Each group has its own settings under Stores → Configuration → Advanced → System → Cron (Scheduled Tasks): how often schedules are generated, how far ahead (
schedule_ahead_for), when a pending job counts as missed (schedule_lifetime), how long rows are kept (history_success_lifetime,history_failure_lifetime) and whether the group runs in a separate process. They are saved undersystem/cron/<group>/incore_config_data. -
Consumer jobs run via queue:consumers:start
Message queue consumers (async order placement, bulk API operations, mass attribute updates, custom RabbitMQ work) don’t fit the once-a-minute loop. They run as their own processes through
bin/magento queue:consumers:start <consumer>, either kept alive by supervisord or systemd, or started by theconsumers_runnercron job using thecron_consumers_runnersettings inapp/etc/env.php. On small servers I setonly_spawn_when_message_availableandmax_messagesthere, so idle consumers don’t pile up and eat the CPU.
Three environments, three cron strategies
A live store needs cron running all the time, but the setup differs by environment. This is how I handle the three I work with.
-
Production: always on, always monitored
Every live store needs cron. Without it, async order emails don’t go out, indexers set to Update by Schedule fall behind, scheduled price changes don’t apply, currency rates stop updating and sitemaps don’t regenerate. I install the crontab with
cron:install, alert on recenterrorormissedrows incron_schedule, and keepvar/log/magento.cron.login the log rotation. -
Local dev: optional, but reindex manually
On a local machine you can leave cron off to save CPU. Then you have to run
bin/magento indexer:reindexyourself after catalog changes, because no scheduled indexer run will happen. In my Docker setups I runbin/magento cron:runby hand when I need to test a cron job, instead of adding a crontab on my own machine. -
Adobe Commerce Cloud: pre-configured
On Adobe Commerce Cloud the platform runs cron for you from the
cronssection of.magento.app.yaml, and consumers can be started through theCRON_CONSUMERS_RUNNERdeploy variable in.magento.env.yaml. You don’t install anything, but monitoring is still your job. New Relic and thecron_scheduletable are where I look first.
Four cron mistakes that silently kill production
Every cron debug ticket I’ve handled in the last 3 years traces back to one of these four. Audit your setup against them now, cheaper than the post-mortem.
-
Forgetting cron:install after a server migration
The single most common silent-killer in Magento ops. The codebase deploys cleanly to the new server, the storefront loads, the admin loads, but no host crontab entries exist, so no Magento jobs run. Symptoms appear hours later: indexers stale, orders not emailing, sitemap missing. Always re-run
bin/magento cron:installas part of your post-migration checklist and verify withcrontab -lon the same Linux user that owns the codebase. -
Running consumers in the same minute as default
The
consumersgroup spawns long-running workers that can hold large amounts of memory (async-indexing on a 500k-SKU catalog, bulk import workers). Running it on the same one-minute cadence asdefaultpiles up overlapping PHP processes and triggers OOM kills under load. Best practice: scheduleconsumerson a longer interval (e.g. every 5 minutes) or run it as a supervisor-managed daemon withqueue:consumers:start <name> --single-thread. -
Not monitoring missed/error rows
A "missed" row in
cron_schedulemeans the job was scheduled but never ran, cron itself stopped, a previous run held the lock too long, or the host CPU was saturated. An "error" row means the job ran but threw. Both fail silently from a customer’s perspective until something visible breaks (orders stop emailing, prices stop updating). Add a 5-minute alert:SELECT COUNT(*) FROM cron_schedule WHERE status IN ("missed", "error") AND scheduled_at > NOW() - INTERVAL 1 HOUR. -
Cron user differs from web user
When the host crontab runs as
root(or a different Linux user) but the web server runs aswww-data, every cron-written file (cache, generated/, var/log) ends up owned by the wrong user, the next web request can’t write to it and Magento crashes with permission errors. Always install cron under the same user that runs PHP-FPM. On most stacks that’ssudo -u www-data crontab -e, notcrontab -eas root.
Six questions devops teams ask about Magento cron
My cron isn’t running, what’s the first thing to check?
Run crontab -l on the Linux user that owns the codebase (usually www-data or your deploy user, NOT root). You should see three lines invoking bin/magento cron:run --group=default|index|consumers. If they’re missing, run bin/magento cron:install. If they’re present but jobs still aren’t running, check var/log/cron.log and var/log/magento.cron.log for the last lines, you’ll typically see a PHP fatal, a permission denied, or a database connection error. The third place to look is SELECT * FROM cron_schedule ORDER BY scheduled_at DESC LIMIT 20, if everything is "missed", cron isn’t executing at all; if it’s "error", individual jobs are throwing.
My cron_schedule table grew to millions of rows, how do I fix it?
Two-step fix. Short term: truncate the old rows. DELETE FROM cron_schedule WHERE scheduled_at < NOW() - INTERVAL 7 DAY, this is safe; the cron history is informational, not state. Long term: tune the history-lifetime configs in Stores → Configuration → Advanced → System → Cron. Defaults are history_success_lifetime = 60 minutes and history_failure_lifetime = 600 minutes, but the cleanup cron itself only runs every 10 minutes, on a high-traffic store with hundreds of jobs/minute the table can still grow large. Drop both lifetimes and run the cleanup job more aggressively, or add a nightly DB cleanup script. The root cause is almost always a misbehaving custom cron job that schedules itself far too frequently.
Should the cron user be different from the web user?
No, they should be the same Linux user. When cron runs as a different user (commonly root while web runs as www-data), every file the cron writes (cache files, generated/ artifacts, log entries, sitemap.xml) ends up owned by the wrong user. The next web request can’t modify those files and you get permission-denied errors that look unrelated to cron. Standard pattern: install the crontab under the web user with sudo -u www-data crontab -e, or whatever user owns var/ and generated/ in your stack. On Docker setups, the Magento container should run as one user end-to-end.
Magento cron vs system cron, what’s the difference?
"System cron" (or "host cron") is the Linux scheduler, the daemon that reads /var/spool/cron/<user> and fires jobs at scheduled times. "Magento cron" is the workload Magento publishes to that scheduler: a registry of jobs (defined in module crontab.xml files) plus the runner (bin/magento cron:run) that the system cron invokes once per minute. Magento doesn’t replace system cron, it depends on it. Without a working host crontab there is no Magento cron. The three lines cron:install writes are how Magento connects its job registry to the host scheduler.
Adobe Commerce Cloud cron, is it auto-managed or do I configure it?
Mostly auto-managed, but not entirely. Adobe Commerce Cloud pre-configures the three cron groups (default, index, consumers) to run on the platform’s worker container at one-minute intervals, you don’t install anything. Where you DO own configuration: the workers stanza in .magento.app.yaml for long-running queue consumers (set the right commands.start and resource limits), the cron history-lifetime tuning in admin, and monitoring, New Relic + cron_schedule dashboards are still your team’s responsibility. Adobe doesn’t alert you when jobs go to "missed" or "error".
Can you show a custom cron task example?
Two pieces. First, etc/crontab.xml in your module declares the schedule: <config><group id="default"><job name="myvendor_clean_old_carts" instance="MyVendor\Cart\Cron\CleanOldCarts" method="execute"><schedule>0 3 * * *</schedule></job></group></config>. The schedule string is standard cron syntax (here: 03:00 daily). Second, the PHP class with an execute() method does the work, inject any repository/service via constructor and write your business logic. Once the module is enabled and setup:upgrade has run, the next cron tick will pick up the job and start scheduling rows in cron_schedule. Use the default group for short jobs (< 5 minutes), index for indexer-style work, and consumers for queue-message processing.
Cron broken on a live store? Or planning a migration?
I run Magento cron audits as part of every performance engagement, host crontab, schedule_lifetime tuning, consumer supervision, monitoring on cron_schedule. Fixed-price, written report, 24-hour turnaround.