Skip to content
5.0 Upwork, 100+ reviews Adobe Certified since 2021 New: become your own Magento developer Learn more →
Magento glossary

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.

How it works
  1. cron:install adds the host crontab entry 01
  2. crontab.xml registers tasks per module 02
  3. cron_schedule table is the run journal 03
  4. Each group has its own schedule and history settings 04
  5. Consumer jobs run via queue:consumers:start 05
Each step is explained below
  • 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

How it works

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.

  1. cron:install adds the host crontab entry

    Running bin/magento cron:install as the user that owns the code adds one block to that user’s crontab: a line that runs bin/magento cron:run every minute and appends the output to var/log/magento.cron.log. I check it with crontab -l. The server’s cron daemon is the real scheduler; Magento only decides which jobs are due each time it is called.

  2. 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, and cron:run reads it each minute to decide what is due.

  3. cron_schedule table is the run journal

    Each scheduled run gets a row in the cron_schedule table, and its status moves from pending to running to success, missed or error. A missed row means the job didn’t start within the “Missed if Not Run Within” window. An error row keeps the exception text in the messages column. When I audit a store, my first query counts the error and missed rows from the last day.

  4. 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 under system/cron/<group>/ in core_config_data.

  5. 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 the consumers_runner cron job using the cron_consumers_runner settings in app/etc/env.php. On small servers I set only_spawn_when_message_available and max_messages there, so idle consumers don’t pile up and eat the CPU.

When to use

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 recent error or missed rows in cron_schedule, and keep var/log/magento.cron.log in 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:reindex yourself after catalog changes, because no scheduled indexer run will happen. In my Docker setups I run bin/magento cron:run by 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 crons section of .magento.app.yaml, and consumers can be started through the CRON_CONSUMERS_RUNNER deploy variable in .magento.env.yaml. You don’t install anything, but monitoring is still your job. New Relic and the cron_schedule table are where I look first.

Common mistakes

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.

FAQ

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.

Performance audit

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.

Kishan Savaliya Online 10 AM to 7 PM IST Chat on WhatsApp