Offloading Long-Running Work in Moodle with the Task API
Learn how to offload long‑running Moodle work—like generating a large course‑completion CSV—using the Adhoc Task API to keep the UI responsive.
20 Mar 2026, 14:30 UTC

Problem: Long operations block the user interface
When a teacher requests a bulk course‑completion CSV for a large site, the PHP script must query hundreds of thousands of rows, format the data, and write a file. Running this work inside the HTTP request makes the page hang, can exceed PHP time‑limits, and ties up a web worker that could serve other users.
Thesis: Use Moodle’s Adhoc Task API to move the heavy work to a background CLI process
The Task API lets you serialize a unit of work into the task_adhoc table, return immediately to the browser, and let the system cron (or a dedicated task processor) execute the job later. This gives the user a responsive interface while the report runs in the background.
Understanding Adhoc vs. Scheduled tasks
- Adhoc tasks are one‑off jobs queued instantly. They extend
\core\task\adhoc_task, are stored intask_adhoc, and are picked up by the next cron run. - Scheduled tasks recur according to a cron expression defined in
db/tasks.php. They use thetask_locktable to prevent overlapping executions across clustered cron nodes.
For a report that is triggered on demand, an Adhoc task is the appropriate choice.
Worked example: CSV course‑completion report
1. Define the task class (file: classes/task/generate_completion_csv.php)
namespace tool_report\task;
use \core\task\adhoc_task;
class generate_completion_csv extends adhoc_task {
protected $courseid;
protected $filters; // array of optional filters
public function __construct($courseid, array $filters = []) {
$this->courseid = $courseid;
$this->filters = $filters;
parent::__construct();
}
public function get_name() {
return 'Generate course completion CSV';
}
public function execute() {
global $DB;
// Minimal CLI bootstrap – no $USER, $PAGE, or session.
// Re‑fetch any needed objects from IDs stored in the constructor.
$course = $DB->get_record('course', ['id' => $this->courseid], '*', MUST_EXIST);
// Build SQL using $this->filters …
$sql = "SELECT u.id, u.firstname, u.lastname, cf.value FROM {user} u …";
$rs = $DB->execute_read_only_sql($sql);
// Stream rows to a temporary file in the draft area.
$fs = get_file_storage();
$draftitemid = file_get_submitted_draft_itemid('draft');
$fs->delete_area_files($draftitemid);
$file = $fs->create_file_from_pathname($draftitemid, 'report', 'completion.csv', 0, '/', fopen('php://temp', 'w'));
$writer = fopen($file->get_path(), 'w');
foreach ($rs as $row) {
fputcsv($writer, (array)$row);
}
fclose($writer);
// Notify the user – store a message or send email.
// For brevity, we just set a note in the task custom data.
$this->set_custom_data(['filepath' => $file->get_path()]);
}
}
2. Queue the task from a web page (e.g., a local plugin report page)
require_once(__DIR__ . '/../../config.php');
require_login();
$courseid = required_param('courseid', PARAM_INT);
$filters = optional_param('filters', [], PARAM_RAW);
// Instantiate and queue the task.
$task = new \tool_report\task\generate_completion_csv($courseid, $filters);
\core\task\manager::queue_adhoc_task($task);
// Return a polling URL or show a message.
$output = $OUTPUT->header();
$output .= $OUTPUT->notification(get_string('reportqueued', 'tool_report'));
$output .= $OUTPUT->continue_button(new moodle_url('/report/tool_report/index.php'));
$output .= $OUTPUT->footer();
echo $output;
3. Process the task
Run the system cron (recommended every minute):
php admin/cli/cron.php
Or, for high‑volume sites, start a dedicated task processor:
php admin/tool/task/cli/process_adhoc_tasks.php --max-tasks=20
Monitoring and troubleshooting
- Check the
task_adhoctable:SELECT * FROM task_adhoc WHERE classname LIKE '%generate_completion_csv%'; - View the admin UI: Site administration → Server → Tasks → Adhoc task log shows state, runtime, memory peak, and any exception trace.
- CLI output (when running cron) includes lines like
[INFO] Starting adhoc task tool_report\task\generate_completion_csvand[INFO] Completed adhoc task …. - If the task fails, the
failurescolumn stores the exception trace; inspect it via the UI or directly in the table.
Trade‑offs and limitations
Eventual consistency: The user does not see the CSV immediately. They must reload the page or wait for an email notification after the task finishes.
Queue depth monitoring: If cron stops, Adhoc tasks accumulate. Set up a simple alert on the task_adhoc row count (e.g., > 1000) to catch a stalled cron.
No session or $USER: Tasks run in a minimal CLI bootstrap; any required context must be rebuilt from IDs passed to the constructor.
Granularity: Reserve Adhoc tasks for work that takes more than a couple of seconds or performs external I/O (file generation, API calls, email). Trivial work adds unnecessary overhead.
Actionable closing
To adopt this pattern in your own Moodle plugins:
- Create a class extending
\core\task\adhoc_taskand implementexecute()with the long‑running logic. - From the controller or form that triggers the work, instantiate the task and call
\core\task\manager::queue_adhoc_task(). - Return a friendly message and optionally a polling URL or email notification.
- Ensure the system cron runs every minute (or run the task processor daemon) and monitor the Adhoc task log for failures.
By moving heavy processing out of the request cycle, you keep Moodle responsive for teachers and students while still delivering the data they need.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.