Configuring Moodle Scheduled Tasks for Reliable Background Processing
Configure Moodle's cron system via PHP CLI and systemd timers for reliable background task execution. Covers manual verification, scheduling methods, task interval tuning, log monitoring, and recovery for stuck tasks or timezone mismatches.
27 Nov 2025, 13:53 UTC

The Problem: Silent Task Failures
Moodle's scheduled task system handles everything from session cleanup and forum digests to plugin-specific jobs like certificate generation or LTI grade sync. When the underlying cron mechanism misfires — wrong user, overlapping runs, PHP-FPM timeouts — tasks either stack up silently or never run at all. The symptom is often stale "Last run" timestamps in Site administration > Server > Scheduled tasks while the site appears functional.
Takeaway: Run cron.php via PHP CLI under a dedicated system user, scheduled through either a traditional crontab or a systemd timer (preferred for logging and concurrency control). Verify execution within 10 minutes using the admin UI and structured logs.
Prerequisites
- Moodle 3.11+ (stable task API; 4.0+ adds concurrent execution flag)
- PHP CLI installed and on
PATHfor the application server user (typicallywww-dataorapache) - Root/sudo access to install cron entries or systemd units
- Known absolute path to Moodle root (e.g.,
/var/www/moodle) - Database backup before any manual lock clearing
Step 1: Verify PHP CLI and Manual Run
Confirm the CLI binary matches the web PHP version and extensions:
php -v
php -m | grep -E 'pdo_mysql|mbstring|xml|curl|zip|gd|intl|opcache'
Run a manual cron execution as the web user to surface permission or path issues immediately:
sudo -u www-data php /var/www/moodle/admin/cli/cron.php
Expected: Zero errors on stdout. If you see "Database connection failed" or "Class not found", fix config.php paths or PHP extensions before scheduling.
Step 2: Choose Scheduling Method
Option A: Traditional Crontab (Simple, Ubiquitous)
Edit the crontab for the web user (not root):
sudo crontab -u www-data -e
Add a single line — no more frequent than 1 minute:
* * * * * /usr/bin/php /var/www/moodle/admin/cli/cron.php >> /var/log/moodle/cron.log 2>&1
Create the log directory first:
sudo mkdir -p /var/log/moodle && sudo chown www-data:www-data /var/log/moodle
Risk: Crontab provides no built-in concurrency guard; Moodle's internal locking prevents overlap but increases database contention if the previous run hasn't finished.
Option B: Systemd Timer (Recommended for Production)
Create /etc/systemd/system/moodle-cron.service:
[Unit]
Description=Moodle Scheduled Task Runner
After=network.target mariadb.service
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/moodle
ExecStart=/usr/bin/php admin/cli/cron.php
StandardOutput=append:/var/log/moodle/cron.log
StandardError=append:/var/log/moodle/cron.log
# Optional: limit resources
MemoryMax=512M
CPUQuota=50%
[Install]
WantedBy=multi-user.target
Create /etc/systemd/system/moodle-cron.timer:
[Unit]
Description=Run Moodle cron every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Persistent=true
[Install]
WantedBy=timers.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now moodle-cron.timer
Advantage: journalctl -u moodle-cron gives structured logs with timestamps; systemd prevents concurrent service starts automatically.
Step 3: Ensure Single Scheduler
Only one mechanism must trigger cron.php. Disable any of these if present:
- Web-based cron (external
wget/curlhitting/admin/cron.php) - Duplicate crontab entries for root or other users
- Multiple systemd timers pointing to the same Moodle instance
Check with:
sudo crontab -l -u www-data
sudo crontab -l -u root
systemctl list-timers --all | grep moodle
Step 4: Adjust Task Intervals (If Needed)
Most core tasks use sensible defaults. To change a task's schedule:
- Go to Site administration > Server > Scheduled tasks
- Click the edit icon (pencil) next to the task name
- Set Minute, Hour, Day, Month fields using standard cron syntax
- Save changes
Example: Reduce "Clean up old sessions" from daily to every 6 hours for high-traffic sites: 0 */6 * * *.
Caution: Custom scheduled tasks must implement \core\task\scheduled_task and be declared in the plugin's db/tasks.php. Missing uninstall.php leaves orphaned rows in mdl_task_scheduled after plugin removal.
Expected Checks (Within 10 Minutes)
Admin UI Verification
Navigate to Site administration > Server > Scheduled tasks. At least three core tasks should show recent "Last run" timestamps:
core\task\session_cleanup_task(Clean up old sessions)core\task\failed_login_notification_task(Send failed login notifications)core\task\legacy_plugin_cron_task(Legacy cron processing for plugins)
No task should remain in "Running" state longer than 2× its schedule interval.
Log Inspection
Crontab:
tail -f /var/log/moodle/cron.log
Systemd:
journalctl -u moodle-cron -n 20 --no-pager
Look for "Cron script completed correctly" or specific task names. Errors like "Maximum execution time exceeded" or "Lock wait timeout" indicate configuration issues.
Database Query Spot-Check
SELECT name, lastruntime, nextruntime, disabled
FROM mdl_task_scheduled
WHERE disabled = 0
ORDER BY nextruntime;
Confirm nextruntime is in the future and reasonable (not years ahead). lastruntime should be within the last few minutes for frequent tasks.
Failure Recovery
Stuck Scheduled Tasks ("Running" State Persists)
Moodle sets a lock in mdl_task_scheduled when a task starts. If the process dies, the lock remains. Clear it manually:
sudo -u www-data php /var/www/moodle/admin/cli/cron.php --force
The --force flag ignores the lock and runs all due tasks. Use sparingly — it bypasses concurrency protection.
Stuck Ad-Hoc Tasks (Queue Backlog)
Ad-hoc tasks (one-off jobs like email sending) live in mdl_task_adhoc. If they pile up:
- Backup the table:
mysqldump moodle mdl_task_adhoc > adhoc_backup.sql - Truncate:
TRUNCATE TABLE mdl_task_adhoc; - Re-run cron to regenerate critical tasks
Warning: This discards pending emails, grade syncs, etc. Only do this if the queue is clearly corrupted (e.g., thousands of identical failed tasks).
Cron Timeout / Memory Exhaustion
If cron.php hits max_execution_time or memory_limit:
- Increase CLI limits in
/etc/php/8.x/cli/php.ini:
max_execution_time = 300
memory_limit = 512M - Or split heavy work into a custom scheduled task with the
concurrentflag (Moodle 4.0+):
class myplugin_long_task extends \core\task\scheduled_task {
public function get_name() { return 'My long task'; }
public function execute() { /* chunked work */ }
public function is_concurrent() { return true; }
}
Timezone Mismatch
Tasks run at unexpected local times when server timezone, PHP date.timezone, and Moodle $CFG->timezone differ. Align all three:
# Server
sudo timedatectl set-timezone America/New_York
# PHP CLI (php.ini)
date.timezone = America/New_York
# Moodle config.php
$CFG->timezone = 'America/New_York';
Verify with SELECT NOW(); in the database and date in shell.
Monitoring Health Check
Add a lightweight endpoint (local plugin or external script) that queries:
SELECT name, lastruntime, nextruntime
FROM mdl_task_scheduled
WHERE disabled = 0
AND lastruntime < UNIX_TIMESTAMP() - (2 * (nextruntime - lastruntime));
Alert if any rows return — meaning a task hasn't run within 2× its expected interval. This catches silent failures before users notice missing digests or uncleaned sessions.
Limitations and Gotchas
- Shared hosting without CLI access: Must use legacy web cron (
wget -q -O /dev/null https://site/admin/cron.php). Accept reduced reliability — PHP-FPM workers tie up, memory limits apply, no structured logs. - PHP 8.1+ deprecation:
create_function()used in some legacy third-party tasks throws deprecation warnings. Test all plugins after PHP upgrade; patch or replace offending tasks. - Concurrent execution (Moodle 4.0+): Only tasks with
is_concurrent() = truerun in parallel. Core tasks are sequential by default. Don't assume parallelism without checking each task's class. - No automatic retry: Failed tasks log an error and wait for next schedule. No built-in backoff or dead-letter queue — implement in custom tasks if needed.
Quick Verification Checklist
sudo -u www-data php /var/www/moodle/admin/cli/cron.php→ zero errors- Admin UI shows updated "Last run" for 3+ core tasks
systemctl status moodle-cron.timershows active (or crontab entry present)journalctl -u moodle-cron -n 20or/var/log/moodle/cron.logshows clean completion- Database query shows future
nextruntimevalues - Stop cron for 10 min, restart → tasks run once, not in burst
If all pass, your scheduled task infrastructure is solid. Re-run the checklist after any PHP upgrade, Moodle version change, or plugin installation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.