What is Magento Cron?
Misconfigured cron is the #1 silent-failure source in Magento. Adobe Certified Magento developer explains what cron is, how the 3 groups work, when to use each, and the four mistakes that bite stores in production.
- Cron groups 3 (default · index · consumers)
- Run interval Every minute (system cron)
- Failure mode #1 silent killer in Magento
The five moving parts of Magento cron
Magento cron is five primitives in a chain: host crontab, module crontab.xml, the cron_schedule table, per-group runtime config, and the consumer process model. Once you understand all five, every cron debug is mechanical.
01cron:install writes 3 host crontab entries
Running bin/magento cron:install appends three lines to the host user’s crontab, one per cron group (default, index, consumers). Each line schedules bin/magento cron:run --group=<name> at one-minute intervals. The host crontab is the actual scheduler; Magento cron is just the workload Magento publishes for it.
02crontab.xml registers tasks per module
Each Magento module declares its scheduled tasks in etc/crontab.xml, an XML file mapping a job name to a PHP class, a method, a cron group, and a schedule expression (cron string or config-path). At boot, Magento reads every module’s crontab.xml and merges the entries into the in-memory job registry that cron:run consults each minute.
03cron_schedule table is the run journal
Every job invocation gets a row in the cron_schedule MySQL table, transitioning pending → running → success | missed | error. Pending rows are picked up on the next minute; missed rows mean the cron didn’t run within schedule_lifetime; error rows include the exception message. A healthy store has zero missed/error rows older than 1 hour.
04Each task has max-runtime + history-lifetime
Per-group config controls how long a task may run before being killed (max_running_time), how far ahead jobs are scheduled (schedule_ahead_for), how long completed rows stay (history_success_lifetime / history_failure_lifetime), and how often the table is pruned. Tunable via system.xml under Stores → Configuration → Advanced → System → Cron.
05Consumer jobs run via queue:consumers:start
Long-running message-queue consumers (async order place, bulk product import, async-indexing, custom RabbitMQ workloads) don’t fit the once-per-minute cron loop. They run as separate processes via bin/magento queue:consumers:start <consumer>, kept alive by a supervisor (systemd, supervisord, Docker) or invoked from the consumers cron group with --max-messages + --single-thread tuned for your workload.
Three environments, three cron strategies
Cron isn’t optional in production, but the right setup differs by environment. Here’s the matrix.
Production: always on, always monitored
Every production Magento store needs cron running. Without it: order confirmation emails don’t send, indexers go stale, scheduled price changes don’t fire, currency rates freeze, sitemaps don’t regenerate, customer notifications stall. Set up the host crontab via cron:install, alert on cron_schedule.status = "error" or "missed" rows older than 5 minutes, and pipe the cron stdout/stderr into your log aggregator.
Local dev: optional, but reindex manually
On a local dev box you can leave cron disabled to keep CPU free, but then you must remember to run bin/magento indexer:reindex after catalog edits, since the cron-driven indexer pass won’t happen. Many devs ship a cron:run watch loop in their dev container so production parity is maintained without polluting the host crontab.
Adobe Commerce Cloud: pre-configured
On Adobe Commerce Cloud, cron is pre-configured by the platform, the three groups run automatically at one-minute intervals on the worker container, and consumers run via the platform’s workers stanza in .magento.app.yaml. You don’t install anything, but you still own monitoring: New Relic + the cron_schedule dashboard remain your responsibility.
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:install as part of your post-migration checklist and verify with crontab -l on the same Linux user that owns the codebase.
Running consumers in the same minute as default
The consumers group 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 as default piles up overlapping PHP processes and triggers OOM kills under load. Best practice: schedule consumers on a longer interval (e.g. every 5 minutes) or run it as a supervisor-managed daemon with queue:consumers:start <name> --single-thread.
Not monitoring missed/error rows
A "missed" row in cron_schedule means 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 as www-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’s sudo -u www-data crontab -e, not crontab -e as root.
Where cron fits in the wider Magento stack
- Magento 2 performance optimization Cron tuning is one lever, the full performance playbook covers indexers, cache, Varnish, DB.
- Adobe Commerce Cloud deployment How cron is pre-configured on the Cloud platform, what you do and don’t own.
- What is Multi-Source Inventory MSI reservations are pruned by a cron job, another reason a healthy cron matters.
- Magento multi-store development Multi-store catalog price rules, currency rates, sitemaps, all cron-driven.
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/ 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: . 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?
Two paragraphs is enough: scope, price and timeline come back in writing.