Moodle Cron Execution Strategy: Choosing Between CLI, Web Endpoint, and Systemd Timers
A decision guide for Moodle administrators comparing CLI cron, web cron, and systemd-managed timers across security, reliability, and hosting constraints.
10 Jul 2026, 18:46 UTC

The problem: a minute-by-minute process with no safe default
Moodle relies on a background process—cron—to send queued email, run scheduled reports, clean up temporary files, and execute plugin maintenance. If that process stops, the site does not crash immediately, but notifications stall, backups fail silently, and long-running tasks accumulate. The engineering decision is not whether to run cron, but which execution mechanism matches your access level and risk tolerance.
Useful takeaway: on a dedicated server, run the CLI script under a systemd timer for observability and catch-up. On shared hosting without shell access, protect the web endpoint with a remote password and accept the timeout trade-off.
Constraints that drive the choice
- Frequency: Moodle expects cron to fire at least once per minute; less frequent runs delay every dependent task.
- Exposure: an unauthenticated web endpoint allows anyone to trigger heavy jobs, consuming PHP workers and database connections.
- Environment: shared hosting typically provides HTTP and a control panel, but no shell. A VPS or dedicated server provides root and systemd.
- Runtime parity: the CLI PHP binary may load a different
php.inithan the web server module, changing memory limits and available extensions.
Supported options at a glance
| Factor | CLI via crontab | Web endpoint (/admin/cron.php) | Systemd timer + CLI service |
|---|---|---|---|
| Required access | Shell + crontab | HTTP only | Root or sudo for systemd |
| Security posture | No HTTP surface | Needs $CFG->cronremotepassword or IP allowlist | No HTTP surface |
| Timeout risk | CLI PHP limits (often higher) | Web max_execution_time and WAF rules | CLI PHP limits |
| Observability | Exit codes, file logs | Web server access logs | journalctl + exit codes |
| Catch-up after downtime | Depends on cron daemon | External ping required | Persistent=true catch-up |
| Best fit | Dedicated server, simple setup | Shared hosting | Dedicated server, ops maturity |
Trade-offs in practice
CLI execution
Running php /path/to/admin/cli/cron.php directly is the fastest and most secure path because it bypasses the web server entirely. It avoids web-server memory ceilings and makes monitoring straightforward: a non-zero exit code signals failure. The cost is dependency on an OS-level scheduler such as cron or systemd, and the need to confirm that the CLI PHP binary has the same extensions as the web SAPI.
Web cron
Hitting /admin/cron.php over HTTP works on shared hosting where no shell exists. It is portable, but every invocation pays HTTP and authentication overhead. Long tasks can be terminated by max_execution_time or intercepted by a web application firewall. Without a strong $CFG->cronremotepassword or an IP allowlist, the endpoint is an unauthenticated trigger for resource-intensive jobs.
Systemd-managed CLI
Wrapping the CLI script in a systemd service and timer adds structured logging through journald, restart semantics, and catch-up execution after downtime. The trade-off is operational coupling: Moodle availability now depends on host unit files, and you need elevated privileges to install them.
Concrete implementation: systemd timer on a dedicated server
The following example assumes Moodle is installed at /var/www/html, the web server user is www-data, and the PHP CLI binary is /usr/bin/php. Adjust these placeholders to match your deployment.
1. Create the service unit
Write the file /etc/systemd/system/moodle-cron.service as root:
[Unit]
Description=Moodle scheduled tasks
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/bin/php /var/www/html/admin/cli/cron.php --verbose
Risk: running as root is unnecessary and dangerous; use the same unprivileged user that owns the Moodle files. If --verbose is unsupported in your version, run php /var/www/html/admin/cli/cron.php --help first to confirm available flags.
2. Create the timer unit
Write /etc/systemd/system/moodle-cron.timer:
[Unit]
Description=Fire Moodle cron every minute
[Timer]
OnCalendar=*:*:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=*:*:00 means the top of every minute. Persistent=true queues a catch-up run if the machine was powered off when the timer should have fired.
3. Activate and verify
- Run
systemctl daemon-reloadas root to load the new units. - Enable and start the timer:
systemctl enable --now moodle-cron.timer. - Check that the timer is waiting:
systemctl status moodle-cron.timer. - Inspect the most recent execution:
journalctl -u moodle-cron.service -n 20 --no-pager.
Alternative: plain crontab
If you prefer not to manage systemd units, add an entry to the web user's crontab with crontab -e -u www-data:
* * * * * /usr/bin/php /var/www/html/admin/cli/cron.php >/dev/null 2>&1
This is simpler, but it lacks journald integration and does not guarantee catch-up after downtime. It also does not serialize overlapping runs; if a previous execution hangs, a new one may start.
Shared-hosting fallback: protected web cron
When shell access is unavailable, set a remote password in config.php:
$CFG->cronremotepassword = 'replace-with-a-long-random-string';
Then schedule an external service or control-panel cron job to request:
https://your-moodle-site.example/admin/cron.php?password=replace-with-a-long-random-string
Limitation: the password appears in URLs and may be logged by proxies or the web server. Rotate it if logs are exposed. If your Moodle version supports $CFG->cronclionly and it is enabled, the web endpoint is disabled entirely and this path will not work.
Validation checklist
- CLI smoke test: from the Moodle root, run
php admin/cli/cron.php --verboseas the web user. Confirm the exit code is0(echo $?) and that task output appears without PHP fatal errors. - UI confirmation: open Site administration > Server > Scheduled tasks and verify that the
Last runcolumn advances after each cycle. - systemd health: run
systemctl status moodle-cron.serviceandjournalctl -u moodle-cron.serviceto ensure the timer fires and no permission errors occur. - Runtime parity: compare
php -mfrom the CLI against the web server's loaded modules. Required extensions such asmbstring,curl, andxmlmust be present in both contexts.
Version-sensitive behavior and limitations
Configuration variable names and CLI flags can change across major Moodle releases. Always verify the exact syntax against your deployed version before relying on it. Plugin tasks may register differently between releases, so a task that appears in the Scheduled tasks UI under one version may be absent or renamed in another.
Moodle uses lock files to prevent concurrent cron runs, but external wrappers should still serialize executions. A systemd oneshot service will not start a second instance while the first is active; a plain crontab entry has no such guard. On shared hosting, web cron timeouts can abort long tasks silently, so prefer CLI where available or split heavy jobs into smaller scheduled tasks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.