When a Moodle scheduled task crashes mid-run, the lock it held can prevent the task from running again. This guide explains how Moodle's task locking actually works, how to identify a stuck task, and how to clear it without disrupting active tasks.

How Moodle task locking works (and why it usually self-heals)

Every scheduled and ad-hoc task acquires a lock before it runs, so two processes cannot run the same task at once. The important detail is where that lock lives, because it determines whether a crash leaves a stuck lock behind.

By default Moodle does not use file locks. When $CFG->lock_factory is unset, Moodle auto-selects a database lock factory based on your DB: mysql_lock_factory (MySQL/MariaDB GET_LOCK()) or postgres_lock_factory (PostgreSQL advisory locks). These locks are tied to the database connection. When a task process crashes, is OOM-killed, or the server restarts, the connection drops and the database releases the lock automatically and immediately. So on a standard MySQL or PostgreSQL site, a crashed task does not leave a stale lock; the next run acquires the lock normally and the task recovers on its own.

A persistent stuck lock is therefore the exception, not the rule. It happens when the site has been explicitly configured to use a non-connection-based factory, namely the file lock factory (file_lock_factory, locks under $CFG->dataroot/lock/) or the database-record factory (db_record_lock_factory, locks in the lock_db table). Those do not auto-release on a dropped connection, so a crash can strand a lock. Before hunting for a lock to delete, confirm which factory your site uses: if $CFG->lock_factory is not set in config.php, you are on the auto-released DB locks and the cause of a non-running task is almost certainly something other than a stale lock (see the diagnostics below).

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.

Clearing a stale lock (only if you use a non-default factory)

On the default DB lock factory there is nothing to delete: a crashed task's lock is already gone. Manually re-running the task (below) is all that is needed. The steps here apply only if your site explicitly sets a file or database-record lock factory.

File lock factory ($CFG->lock_factory = 'corelockfile_lock_factory'): locks are files under $CFG->dataroot/lock/. Inspect them, and remove only a file older than the task's expected duration with no matching running process:

ls -la /var/moodledata/lock/

Database-record factory ($CFG->lock_factory = 'corelockdb_record_lock_factory'): locks live in the lock_db table. The key for a scheduled task is cron_ followed by its class name:

SELECT id, resourcekey, expires FROM mdl_lock_db;

DELETE FROM mdl_lock_db WHERE resourcekey = 'cron_coretasksend_new_user_passwords_task';

Deleting rows from mdl_lock_db is a direct production database write: take a backup first, and confirm no live process is holding the lock before removing it.

Whichever factory you use, re-run the task manually to confirm it recovers:

php admin/cli/scheduled_task.php --execute='coretasksend_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_turnitintooltwotasksubmission_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 = NULL
WHERE classname = 'coretaskasynchronous_backup_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