When a Moodle scheduled task crashes mid-run, the lock it held can persist indefinitely, blocking every subsequent run. This guide explains how Moodle's task locking works, how to identify a stuck task, and how to clear it without disrupting active tasks.

How Moodle task locking works

Every scheduled task and ad-hoc task acquires a lock before it runs. By default Moodle uses a file-based lock factory: it creates a lock file inside moodledata/lock/ named after the task. While the lock file exists, no other process can start the same task.

When a task finishes normally it removes the lock file. When it crashes — OOM kill, PHP fatal, server restart — the lock file stays. The next scheduled run finds the lock taken and either waits until a timeout or skips the task entirely. This repeats on every subsequent run. From the admin panel everything looks normal: the task is enabled and appears in the list. It just never runs.

Identifying the stuck task

The scheduled task log is the first place to check:

Site administration > Server > Tasks > Scheduled task log

Filter by the task name and look for a run that started but has no matching completion entry, or check the last run time — if it is hours older than the configured schedule, the task is likely locked.

From the database:

SELECT classname, lastruntime, nextruntime, timestarted
FROM mdl_task_scheduled
WHERE timestarted > 0
ORDER BY timestarted DESC;

For ad-hoc tasks (queued jobs from enrolments, messaging, etc.):

SELECT classname, timestarted, timecreated, faildelay
FROM mdl_task_adhoc
WHERE timestarted > 0
ORDER BY timestarted;

A row with a timestarted value and no corresponding completion means the task is either still running or was abandoned mid-run. Cross-check against running PHP processes:

ps aux | grep php

If there is no matching process for the task, the lock is stale.

Finding the lock file

Lock files live in $CFG->dataroot/lock/. The filenames are derived from the task class name. For example, the lock for \core\task\send_new_user_passwords_task will be a file in the lock/ directory with a name based on that class.

ls -la /var/moodledata/lock/

Files older than your expected task duration with no matching running process are safe to remove.

Clearing a stale lock

For a file-based lock, removing the file is sufficient:

rm /var/moodledata/lock/task_<lockname>

If your installation uses a database lock factory ($CFG->lock_factory = '\core\lock\db_record_lock_factory'), locks are stored in mdl_lock_db:

SELECT * FROM mdl_lock_db;

DELETE FROM mdl_lock_db WHERE resourcekey LIKE '%classname%';

After clearing the lock, trigger the task manually to confirm it runs:

php admin/cli/scheduled_task.php --execute='\core\task\send_new_user_passwords_task'

Isolating heavy tasks from the main cron

Some tasks — H5P content sync, Turnitin submission checks, large report generation — can run for several minutes and hold a lock for the entire duration. When they share a cron invocation with fast tasks, a slow run blocks or delays everything that follows.

Run heavy tasks on their own dedicated CLI invocation, separate from the general cron:

# In a separate cron entry:
php /var/www/moodle/admin/cli/scheduled_task.php \
  --execute='\mod_turnitintooltwo\task\submission_scores_sync'

This keeps the general cron moving and gives you independent control over the frequency and timeout of the heavy task.

Persistent ad-hoc task backlogs

Ad-hoc tasks that repeatedly fail accumulate a faildelay value and are retried with increasing backoff. A large backlog combined with a lock issue can cause the queue to grow faster than it drains. Check the queue depth:

SELECT classname, COUNT(*) AS queued, MAX(faildelay) AS max_delay
FROM mdl_task_adhoc
GROUP BY classname
ORDER BY queued DESC;

Tasks with a high faildelay and a large count are candidates for investigation. If the failure is environmental (a third-party API that was down, a missing file), clearing faildelay and resetting timestarted lets them retry immediately:

UPDATE mdl_task_adhoc
SET faildelay = 0, timestarted = 0
WHERE classname = '\mod_assign\task\cron_task'
AND faildelay > 0;

Only do this once you have resolved the underlying cause.

Solin provides custom Moodle and Totara plugin development and operational support.

Contact us