Magento 2 Cron Jobs Not Running? A Developer’s Guide to Debugging
Trace the failure from scheduler to business result—with practical commands, database evidence, and a complete diagnostic module.
Magento cron problems rarely announce themselves clearly. Customers stop receiving expected messages, product updates arrive late, or an integration quietly falls behind. The storefront can remain available while essential background work is failing. Restarting cron without identifying the broken stage often produces another incident tomorrow.
A useful investigation separates the system scheduler, Magento's scheduling engine, and the actual job callback. This guide follows that path with commands, database checks, and a small diagnostic module. Examples target Magento Open Source and Adobe Commerce installations that support PHP modules; adapt deployment steps to your environment.
1. Identify which stage has stopped
Start with one affected job code and its expected business outcome. Record when it last worked, which deployment preceded the failure, and whether other jobs in the same group still complete. “Cron is broken” is a symptom description, not a diagnosis.
Create a short evidence record containing the environment, release identifier, job code, cron group, schedule identifier, and affected time range. Keep these identifiers together when handing the incident to another developer. They prevent a successful test on staging from being confused with recovery on production.
Magento modules declare jobs through etc/crontab.xml, while repeated cron:run invocations drive scheduling and execution.[1] A successful scheduler invocation does not prove that every callback completed or that its downstream operation succeeded.
2. Verify the scheduler and PHP environment
On a conventional Linux installation, inspect the crontab belonging to the Magento filesystem owner. Check the actual PHP binary, application path, output destination, and scheduler service. A crontab entry can exist while the service is stopped or the command points to an old release.
# Run as the Magento filesystem owner.
# Replace this directory with your application root.
cd /var/www/html/magento2
id -un
pwd
crontab -l
command -v php
php -v
php --ini
php -mReplace the example directory with your installation root. Run PHP checks using the exact binary configured for cron. Compare its version, extensions, and configuration with your Magento release requirements; the web server and command line can load different PHP settings.[2]
If an ordinary self-hosted installation lacks its Magento entry, install it as the filesystem owner, then inspect the result. Preserve existing custom scheduling arrangements.[3]
# Only when this installation needs a Magento cron entry.
php bin/magento cron:install
crontab -lAdobe Commerce on cloud infrastructure uses the project's crons configuration in .magento.app.yaml. Follow that platform's workflow rather than editing a managed host crontab. Supported scheduling intervals and controls differ by environment.[4]
Check that output redirection can create or append to its destination. An unwritable log path can prevent a shell command from starting. Also confirm that credentials or variables supplied by an interactive shell are available through the scheduler's supported configuration; a manual login can conceal that difference.
3. Read cron_schedule before changing anything
Use a read-only database session to inspect a small, relevant result set. Replace the job code and add your configured table prefix if necessary. Preserve the rows and messages associated with the incident before history cleanup removes them.
-- Use the correct database and configured table prefix.
-- Replace the example job code with the affected job.
SELECT schedule_id, job_code, status,
created_at, scheduled_at, executed_at, finished_at,
LEFT(messages, 1500) AS diagnostic_message
FROM cron_schedule
WHERE job_code = 'vendor_cron_debug_heartbeat'
ORDER BY schedule_id DESC
LIMIT 40;| Status | What to investigate |
|---|---|
| pending | Is the scheduled time still in the future, or is execution delayed? |
| running | Is a corresponding process active, progressing, or abandoned? |
| missed | Why did the job fail to start within its allowed window? |
| error | Which exception or callback failure was recorded? |
| success | Did the expected business result actually occur? |
Compare timestamps consistently, including server, database, and log timezones. An empty result can mean the job was never scheduled, the wrong database was queried, or old history was removed. Confirm the environment and retention settings before drawing conclusions.
Compare the affected job with a healthy neighbour from the same period. If many unrelated jobs stop together, investigate shared infrastructure first. If one job repeatedly fails while others finish, inspect its declaration and callback. Preserve complete error details privately when a shortened message omits the relevant stack trace.
4. Reproduce with the correct cron group
In a controlled environment, run the affected group as the normal filesystem owner. This executes eligible work across that group, so understand its side effects before running it on production.
php bin/magento cron:run --group=default
# Inspect scheduled_at in cron_schedule.
# On or after the due time, run the group again.
php bin/magento cron:run --group=defaultMagento may need one invocation to generate schedules and another to execute them. The later invocation must happen on or after the relevant scheduled_at time. Two immediate commands are not a guarantee that a future task will execute.[3]
Compare the affected schedule row before and after execution. The group option selects a group, not an individual job. If manual execution works but automatic execution fails, concentrate on scheduler identity, environment variables, working directory, and runtime configuration.
tail -n 100 var/log/cron.log
tail -n 100 var/log/exception.log
tail -n 100 var/log/system.logInspect cron.log alongside exception and application logs. Capture the earliest relevant error and its timestamp; later messages may only describe consequences. Custom logger output may use a different destination.
During a release switch, verify that the scheduler and your terminal resolve the same application directory. A successful command against a new release tells you little about a scheduled process still using the previous path. Record the exact invocation used for reproduction so the comparison remains meaningful.
5. Prove execution with a minimal module
When your custom job never appears, verify module registration, enablement, namespace spelling, and XML placement. A cron declaration belongs under etc, not etc/frontend. The instance must resolve to an instantiable class with the configured public method.
The following complete diagnostic module writes a heartbeat through Magento's logger. Create these four files on development or staging. It deliberately avoids orders, external requests, and database writes from the callback, making scheduling easier to isolate.[5]
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_CronDebug',
__DIR__
);<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_CronDebug">
<sequence>
<module name="Magento_Cron"/>
</sequence>
</module>
</config><?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="vendor_cron_debug_heartbeat"
instance="Vendor\CronDebug\Cron\Heartbeat"
method="execute">
<schedule>* * * * *</schedule>
</job>
</group>
</config><?php
declare(strict_types=1);
namespace Vendor\CronDebug\Cron;
use Psr\Log\LoggerInterface;
class Heartbeat
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function execute(): void
{
$this->logger->info('CronDebug heartbeat', [
'job_code' => 'vendor_cron_debug_heartbeat',
'pid' => getmypid(),
'utc' => gmdate('c'),
]);
}
}Use the development deployment commands below after adding the files. Production should receive the module through your established build and deployment process, including dependency injection compilation when required. Clean the configuration cache after changing cron XML.
php bin/magento module:enable Vendor_CronDebug
php bin/magento setup:upgrade
php bin/magento cache:clean config
php bin/magento module:status Vendor_CronDebugObserve the heartbeat schedule and log entry across several intervals. If it succeeds while the real job fails, the scheduling path works; investigate the real callback and its dependencies. Remove the temporary diagnostic module through your normal deployment workflow after collecting evidence.
The heartbeat's process identifier and timestamp help correlate callback execution with your process and schedule observations. Its logger destination depends on the application's logging configuration. A missing message therefore requires checking logger routing as well as scheduling; use the schedule row and callback evidence together.
6. Inspect schedules, groups, and disabled settings
Check the effective cron expression. If a module uses config_path, inspect the configuration value it resolves to rather than assuming the XML contains the schedule. Confirm the configured job code and group match your investigation.
Custom groups use cron_groups.xml. Review schedule generation frequency, the scheduling horizon, and the allowed start window. schedule_lifetime controls how late a job may start; it is not a maximum runtime for the callback. use_separate_process controls group process separation.[6]
Also check whether deployment configuration disables cron. Adobe documents cron.enabled in env.php; environment overrides can affect the effective setting.[7] Inspect the relevant keys without pasting the entire file into tickets, because it contains credentials.
A separate group can isolate workloads, but it still needs scheduling and enough resources. Increasing the missed-job window may hide a capacity problem without fixing the delay.
Distinguish the frequency of your operating system invocation from the expression attached to an individual Magento job. They are separate schedules. Review their interaction with the group's start window, especially after moving to hosting with a different minimum interval. Document effective values rather than relying on remembered defaults.
7. Investigate long runs and lock contention
A row marked running is not proof that its process still exists. Compare its execution time with active PHP processes, resource usage, deployment events, and operating system logs. A terminated worker may leave incomplete evidence behind.
ps -eo user,pid,ppid,etime,pcpu,pmem,args | grep '[b]in/magento'Use process inspection to identify the relevant worker, then investigate what it is waiting for. Common causes include slow queries, database locks, external HTTP requests without sensible timeouts, and batches that grow with the catalogue.
Repeated lock warnings may indicate overlapping execution, not a broken lock mechanism. Identify the owner and deployment topology before resetting anything. Deleting schedule rows or removing locks while work remains active can allow duplicate processing.
Look for memory exhaustion, worker termination, container restarts, and platform timeouts near the interruption. Break large workloads into bounded batches with durable progress tracking. Measure throughput before adding concurrency: extra workers can increase contention against the same database or overwhelm a downstream service that already responds slowly.
8. Fix callback failures without hiding them
Trace error rows to the original exception. Check constructor dependencies, missing configuration, permissions, database connectivity, and external service responses. Compare the first failure with recent releases, credential changes, and infrastructure changes.
Confirm that the cron user can read required configuration and write the job's output locations. Correct ownership and narrowly scoped permissions instead of making the entire installation writable.[8]
Log the job code, record identifier, attempt, duration, and outcome. Avoid swallowing an exception and returning normally when required work failed. The following catch block belongs inside an existing job whose logger is injected; it records context and preserves failure propagation.
catch (\Throwable $exception) {
$this->logger->error('Scheduled export failed', [
'job_code' => 'vendor_catalog_export',
'exception' => $exception,
]);
throw $exception;
}Before retrying, determine whether the previous attempt completed part of its work. Stable external references, durable checkpoints, and duplicate protection matter when a callback sends messages or creates remote records. A timeout can occur after the destination accepts the operation.
Classify failures before selecting a recovery action. Invalid input needs correction, expired credentials need renewal, and temporary service failures may justify bounded retries. Repeating every failure indefinitely makes a backlog harder to understand and can hide records that will never succeed without a specific change.
9. Separate cron health from downstream health
If cron succeeds but business work remains unfinished, follow the next handoff. A producer can enqueue a message successfully while its consumer is stopped. Check queue depth, consumer configuration, broker connectivity, and individual processing errors.
php bin/magento queue:consumers:list
php bin/magento indexer:statusListing consumers shows available consumer names, not proof that worker processes are healthy. Adobe supports managing consumers through cron or an external process manager; inspect whichever mechanism your deployment actually uses.[9]
For indexing symptoms, inspect indexer status and the actual storefront result. For email, verify delivery beyond message generation. Assign a clear success condition to every job so a scheduler receipt cannot be mistaken for a completed customer outcome.
Carry a correlation reference across asynchronous handoffs where your integration supports it. That lets you connect the cron invocation with the produced message and destination record. Without this link, teams may repeatedly restart healthy scheduling while the actual failure remains inside a consumer or external application.
10. Verify recovery and monitor the right signals
After correcting the cause, observe several automatic cycles without manual intervention. Check new schedules, start delay, runtime, recorded errors, and the expected business result. Compare a representative job before and after the repair.
-- Replace the job code and apply your table prefix.
SELECT schedule_id, scheduled_at, executed_at, finished_at,
TIMESTAMPDIFF(SECOND, scheduled_at, executed_at)
AS start_delay_seconds,
TIMESTAMPDIFF(SECOND, executed_at, finished_at)
AS runtime_seconds
FROM cron_schedule
WHERE job_code = 'vendor_cron_debug_heartbeat'
AND status = 'success'
AND executed_at IS NOT NULL
AND finished_at IS NOT NULL
ORDER BY schedule_id DESC
LIMIT 20;The query compares timestamps already stored on completed schedules. Start delay measures waiting after the planned time; runtime measures execution duration. Use both when distinguishing scheduler starvation from slow application code.
Alert on the last successful completion of critical jobs and the age of overdue work, not merely the presence of a PHP process. Define thresholds from the actual schedule and business tolerance. Include the affected job, environment, recent error, and recovery owner in every actionable notification.
Record the verified root cause and the evidence that confirmed recovery, then add a focused regression check for the failure you actually encountered.