A Moodle cron not running almost always comes down to one of four things: there is no system cron entry, the entry points at the wrong path or PHP binary, the script starts but exits straight away (cron disabled, an upgrade pending or CLI maintenance mode), or cron runs but its task runners are blocked. The fastest diagnosis is to run php admin/cli/cron.php by hand as the web server user and read what it prints. On Moodle 5.1 and later that script lives in the project root, not in public/, which is the most common reason a working crontab breaks after an upgrade.
This guide follows the order we use on client sites: what the problem looks like, the likely causes ranked, a diagnostic procedure, the fix for each cause, how to confirm the fix and how to stop it happening again. Checked against the Moodle 5.3.0 source and the Task API developer documentation (October 2026); script names and settings are the same on 5.1 and 5.2, and the relative path is the same on 4.5 LTS, where the code root is also the web root.
The problem: what Moodle cron does
Moodle does almost all of its background work through the Task API. Scheduled tasks run on a timetable, such as sending forum notification emails, automated course backups and cleanup jobs. Ad hoc tasks are queued by code to run once, as soon as possible or at a set time, such as large course deletions and resets, or the question bank migration that runs after an upgrade to 5.0 or later.
None of these run on their own. As the developer documentation puts it, tasks only run as often as cron runs, and cron should run at least once per minute. "Cron" here means the operating system scheduler starting admin/cli/cron.php, which then picks up whatever scheduled and ad hoc tasks are due. If that script never starts, or starts and exits, the site keeps serving pages while its background work quietly stops.
Symptoms of a stopped cron
Moodle reports the problem itself, in two places:
- The admin notifications page (Site administration home) shows that the
admin/cli/cron.phpscript "has not been run for" a given time and should run every minute, or that it "has never been run". On sites where web cron is allowed, the same message namesadmin/cron.phpinstead. - Site administration > Reports > System status shows the "Cron running" check. It turns to a warning when the last run is more than a minute overdue and to critical after a day.
If cron runs but too rarely, you see a different warning: there was a given time "between the last two runs of the cron maintenance script" and it should run every minute. That points at the schedule, not at a broken script.
Users notice before admins do. Typical reports are forum and notification emails that never arrive, course deletions or resets that stay pending, automated backups missing from the backup report, and completion or badge changes that do not appear. On Site administration > Server > Tasks > Scheduled tasks, the "Last run" column stops moving and many "Next run" values show ASAP.
Root causes, ranked by likelihood
This ranking reflects what we find most often on sites we take over, not a survey.
| Rank | Cause | Typical sign |
|---|---|---|
| 1 | No system cron entry at all | "has never been run", often after a new install or a server move |
| 2 | Wrong script path, often after the 5.1 move to public/ | Cron stopped on the day of an upgrade or migration; PHP reports "Could not open input file" |
| 3 | Script exits at once: cron disabled, upgrade pending or CLI maintenance mode | Last run stops moving; a one-line message when run by hand |
| 4 | Wrong user or wrong PHP binary | Permission errors in moodledata, missing extension errors, or a different PHP version than the web server |
| 5 | Web cron blocked | A URL-based scheduler gets "Sorry, internet access to this page has been disabled by the administrator." |
| 6 | Cron runs but tasks do not | "Concurrency limit reached", stuck entries under Tasks running now, or a growing fail delay |
Diagnostic procedure
Work through these steps in order. Each one rules out a cause from the table.
- Read the status check. Open Site administration > Reports > System status, or from the project root run
sudo -u www-data php admin/cli/checks.php -v --filter=cron. Note the time since the last run. - Find the cron entry. List the crontab of the user that should run cron, for example
sudo crontab -u www-data -l, and also check/etc/cron.d/and any container or orchestration scheduler you use. No entry means cause 1. - Check the path in that entry. On 5.1 and later the script is
<project root>/admin/cli/cron.php. There is nopublic/admin/clidirectory, so an entry pointing there fails. Confirm the file exists at the exact path in the crontab. - Run it by hand, once. From the project root run
sudo -u www-data /usr/bin/php admin/cli/cron.php --keep-alive=0, using the same PHP binary as the crontab.--keep-alive=0makes it run one pass and exit instead of polling for the default three minutes. - Read the first lines of output. A healthy run prints the server time, the tasks it runs and "Cron run completed correctly". The messages in the next section each map to a cause.
- Look at what is running.
php admin/cli/cron.php --listprints the PID, host, type, running time and class of every running task. Compare it with Site administration > Server > Tasks > Tasks running now. - Check individual tasks. On the Scheduled tasks page, look for tasks that are disabled or have a non-zero fail delay, then open the task logs (Site administration > Server > Tasks > Task logs) for the failing ones.
Several cron.php processes in ps is not a fault on its own. Since Moodle 4.2 each process keeps polling for new tasks for the keep alive time (default three minutes), so with a one-minute schedule a few overlap by design.
Fixes for each cause
1. No cron entry
Add one for the web server user (www-data on Debian and Ubuntu; apache or nginx on other distributions) with sudo crontab -u www-data -e:
# Moodle cron, every minute. Adjust the PHP and project root paths.
* * * * * /usr/bin/php /var/www/moodle/admin/cli/cron.php >/dev/null 2>&1
This example is not run on a live site; test on staging first. Use the full path to PHP, because cron runs with a minimal environment. While you are debugging, send the output to a log file instead of /dev/null, and switch back (or add log rotation) once it works.
2. Wrong path after an upgrade or move
Since Moodle 5.1 the web server serves only public/, while config.php, Composer files and admin/cli/ stay in the project root. If you repointed the web server and also edited the crontab to point into public/, point it back at <project root>/admin/cli/cron.php. After a server migration, also check that the crontab moved with the site and names the new directory. Our Moodle hosting requirements article covers the 5.1 directory layout in full.
3. The script exits straight away
| Message when run by hand | Fix |
|---|---|
| "Cron is disabled. Use --force to override." | Re-enable it: php admin/cli/cron.php --enable, or tick Enable cron under Site administration > Server > Tasks > Task processing. |
| "Moodle upgrade pending, cron execution suspended." | Finish the upgrade: php admin/cli/upgrade.php (add --non-interactive in scripts), or complete it from the notifications page. |
| "CLI maintenance mode active, cron execution suspended." | If the maintenance window is over, run php admin/cli/maintenance.php --disable. |
A disabled cron is easy to forget: it is often switched off before a server restart or a deployment, with --disable or --disable-wait, and never switched back on. Add the matching --enable to the same runbook or deployment script.
4. Wrong user or PHP binary
Run cron as the web server user, never as root. Files that a root-owned cron writes into moodledata, such as caches and temporary files, can then not be written by the web server. If that has happened, fix ownership on moodledata before starting cron again.
The PHP CLI is a separate binary with its own php.ini. If /usr/bin/php is a different version from your PHP-FPM pool, or lacks extensions Moodle needs, cron fails while the website works. Use the versioned binary that matches the web server (for example /usr/bin/php8.3 on Debian-based systems) and give the CLI the same extensions and limits. Set the same path in Site administration > Server > System paths > Path to PHP CLI, which the "Run now" button on the Scheduled tasks page needs.
5. Web cron blocked
"Cron execution via command line only" (cronclionly, under Site administration > Security > Site security settings) is on by default, because running cron from a browser can expose privileged information. A scheduler that calls admin/cron.php over HTTP then gets "Sorry, internet access to this page has been disabled by the administrator." Move to the CLI script. If you have no shell access at all, the fallback is to turn cronclionly off and set "Cron password for remote access" (cronremotepassword). Web cron also runs with keep alive disabled.
6. Cron runs, tasks do not
- "Skipping processing of scheduled tasks. Concurrency limit reached." All scheduled task runner slots are taken. The limit is set under Task processing (default 3 for both scheduled and ad hoc tasks; 0 disables processing). Check Tasks running now for a task that has been running for hours, and fix that task rather than raising the limit. The runner lifetime settings (default 30 minutes each) set how old a runner can get before its slot is freed.
- Failing tasks. A task that throws an exception is retried after a minute, then with longer gaps up to 24 hours, shown as the fail delay. Run it alone with debugging:
php admin/cli/scheduled_task.php --execute='\core\task\session_cleanup_task' --showdebugging, replacing the class with the failing one. Fix the cause, then clear the fail delay on the Scheduled tasks page. - Stuck ad hoc tasks. Use
php admin/cli/adhoc_task.php --failedto rerun only failed tasks, or--id=Nfor one task, with--showdebuggingfor detail. By default an ad hoc task gets 12 retries. - Disabled tasks. A task disabled on the Scheduled tasks page, or with
scheduled_task.php --disable, never runs. Re-enable it with--enable='\class\name'or reset its schedule to the defaults. - Locks on clusters. Every node that runs cron must use a lock factory that works across nodes. File locking in moodledata only works where the shared file system supports locking; database locking (
$CFG->lock_factoryin config.php) avoids that dependency.
Slow tasks usually trace back to the database or storage rather than cron. Our Moodle performance optimisation guide covers how we profile them.
Confirm the fix
- Wait two or three minutes, then reload Site administration > Reports > System status. "Cron running" should read "Cron is running frequently", with a time since the last run under a minute or two.
- On the Scheduled tasks page, check that "Last run" values are recent and that the ASAP backlog is shrinking.
- Check the Ad hoc tasks page: the queue should go down over the following runs.
- Trigger something users rely on, such as a forum post with notifications, and confirm the email arrives.
- Leave the log redirection on for a day and check it for PHP errors before you switch it back to
/dev/null.
Preventing it from happening again
- Keep cron in your deployment checklist. Every server move, rebuild and major upgrade should end with a check that the crontab exists, uses the correct project root path and the correct PHP binary.
- Monitor the status check.
admin/cli/checks.phpreturns NRPE-style exit codes (1 for a warning, 2 for critical), so a monitoring agent can run it and alert you, which beats waiting for users to report missing emails. - Pair every disable with an enable. Script
cron.php --disable-waitand--enabletogether around maintenance. - On clusters, use dedicated task runners. Run cron on one or more nodes that do not serve users, and follow Moodle's own advice for keep alive: 0 with dedicated task runners, otherwise a value similar to the ad hoc task concurrency limit, never above the 15 minute maximum.
- Watch the fail delay and ad hoc queue checks. The System status report also has "Tasks max fail delay", "Ad hoc task queue" and "Long running tasks" checks; treat warnings there as early signs of a task that will block others.
For how cron fits into the rest of a Moodle stack, including upgrades and support dates, see our Moodle guide.
How we can help
We run Moodle sites day to day: cron and task runner setup, cluster locking, upgrades across the 5.1 directory change and monitoring that catches a stopped cron before users do. See our Moodle services and LMS maintenance and support, or contact us with what your notifications page shows.
Frequently asked questions
How often should Moodle cron run?
Every minute. Moodle's Task API documentation recommends running cron at least once per minute, because scheduled and ad hoc tasks only run as often as cron does.
Where is the Moodle cron script in Moodle 5.1 and later?
In the project root at admin/cli/cron.php. Since 5.1 the web server serves the public/ directory, but the CLI scripts stay in the project root, so do not point the crontab into public/.
Why does Moodle say cron has never been run?
Moodle has no record of cron starting, which usually means there is no system cron entry for the site, or the entry fails before Moodle loads, for example because of a wrong path or PHP binary. Run the script by hand as the web server user to see the error.
How do I run a single Moodle scheduled task manually?
From the project root run php admin/cli/scheduled_task.php --execute='\core\task\session_cleanup_task' as the web server user, replacing the class with the task you need. Add --showdebugging to see errors. Use --list to see all scheduled tasks.
Is it normal to see several cron.php processes running?
Yes. Each cron process keeps polling for new tasks for the keep alive time, three minutes by default, so with a one-minute schedule a few overlap. The maximum keep alive is 15 minutes.
Free 30-minute consultation
Not sure where to start? Talk to an LMS engineer.
Tell us what platform you run and what you need. You’ll get honest, practical advice - even if the answer is that you don’t need us.
Sources & references
- Task API - Moodle HQ
- Adhoc tasks - Moodle HQ
- admin/cli/cron.php (Moodle 5.3.0) - Moodle HQ
- admin/cli/scheduled_task.php (Moodle 5.3.0) - Moodle HQ
- Cron running status check (Moodle 5.3.0) - Moodle HQ
- Directory restructure in Moodle 5.1 - Moodle HQ
Learn more about this topic with AI
Gemini opens with the prompt copied - paste it into the chat to start.