Moodle Performance Tuning: Caching, Cron and Database Tips
Where a slow Moodle site loses its time and how to fix it: OPcache, cache stores and Redis, session handling, cron and ad hoc tasks, database memory, slow reports and busy quizzes.

A slow Moodle site is almost never slow everywhere for one mysterious reason. The time goes in a small number of known places: PHP compiling the same files on every request, caches written to a slow disk, sessions that make requests queue, a cron job that has fallen behind, and a database short of memory. Moodle performance tuning is the work of checking those places in order.
The order below is the one we follow on sites we take over. It starts with measurement, because changing five settings at once tells you nothing about which one helped.
Measure before you change a setting
Go to Site administration > Development > Debugging and tick Performance info. The footer of every page then shows how long the page took to build, how much memory it used and how the caches behaved. Load four pages as a normal user and write the numbers down: the dashboard, a course with many activities, the gradebook for a large course and one page of a quiz attempt.
Those four numbers are your baseline. Take them again after each change. Leave the debug level itself at a production setting, because developer debugging slows every request.
Alongside that, watch CPU, memory, disk wait and the number of busy PHP workers during a slow period. A site that is slow only at nine in the morning has a capacity problem. One that is slow at three at night has a scheduled task problem.
Get PHP and OPcache right first
OPcache keeps compiled PHP in memory. Without it, or with it set too small, every request recompiles hundreds of files. Moodle's code base is large, so the default limits are often too low. These values are a sound starting point, and the last four are the ones Moodle's documentation marks as required:
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60
opcache.use_cwd = 1
opcache.validate_timestamps = 1
opcache.save_comments = 1
opcache.enable_file_override = 0
Check the fit instead of trusting the numbers. Count the PHP files in your install, plugins included, and keep opcache.max_accelerated_files above that count. After a day of traffic, look at OPcache's status output: if the memory is full or the cache has restarted, raise the limits.
With PHP-FPM, the other setting that matters is pm.max_children: the memory you can give to PHP divided by the size of an average worker. Too high and the server swaps. Too low and requests queue while the CPU sits idle.
Moodle performance tuning starts with the cache stores
Moodle's caching layer, the Moodle Universal Cache (MUC), has three kinds of cache. Application caches are shared by all users and survive between requests. Session caches belong to one user. Request caches live for a single page load. Each kind is mapped to a store, and the defaults put application caches in files inside moodledata.
On one server with a fast local disk, that default is fine. It becomes a bottleneck when moodledata sits on network storage, which is the usual arrangement as soon as there are two web servers. The fix is to move the application cache into memory:
- Install Redis and the PHP Redis extension.
- Open Site administration > Plugins > Caching > Configuration and add an instance of the Redis store. Enter the server address, and give it a short key prefix so its keys cannot collide with anything else in that Redis.
- At the bottom of the page, under "Stores used when no mapping is present", edit the mappings and set the Redis instance for the application cache.
- Reload your four test pages a few times and compare with the baseline.
On a cluster, also point $CFG->localcachedir at a directory on each node's own disk. The Moodle caching documentation describes every store.
One habit to drop: purging all caches when the site feels slow. A purge forces every page to rebuild what it needs, so a busy site gets slower for a while. Purge after a code or theme change, at a quiet time.
Move sessions to Redis and understand the lock
Every logged-in request reads and writes a session. File sessions on shared storage and database sessions both struggle under load. Redis handles them well, and it is set in config.php:
$CFG->session_handler_class = '\core\session\redis';
$CFG->session_redis_host = '127.0.0.1';
$CFG->session_redis_port = 6379;
$CFG->session_redis_database = 1;
$CFG->session_redis_prefix = 'sess_';
$CFG->session_redis_acquire_lock_timeout = 120;
$CFG->session_redis_lock_expire = 7200;
Keep sessions apart from the cache store, with a separate database number or a separate Redis instance. A cache may be emptied or evicted at any time without harm. If the same happens to sessions, everyone is logged out in the middle of their work.
The lock settings matter because Moodle handles one request per session at a time. While one page is being built for a user, that user's other requests wait, so a slow report in one tab freezes their other tabs. The cure is to fix the slow page, not to shorten the timeout.
Keep cron running every minute
Moodle expects cron every minute, started from the command line and not through a web request:
* * * * * /usr/bin/php /path/to/moodle/admin/cli/cron.php >/dev/null
Cron does more than send forum emails. Course completion, automated backups, scheduled reports and search indexing all run as scheduled or ad hoc tasks. When cron falls behind, the site looks broken: learners finish a course and nothing shows it for hours.
Check Site administration > Server > Tasks > Scheduled tasks for tasks with an old "Last run" or a fail delay, and read the task logs for anything that runs for minutes. Two changes fix most backlogs. Move heavy jobs such as automated course backups to the night by editing their schedule. And if ad hoc tasks pile up, run extra workers beside the main cron, as the cron documentation describes:
* * * * * /usr/bin/php /path/to/moodle/admin/cli/adhoc_task.php --execute --keep-alive=59 >/dev/null
The number of ad hoc tasks allowed to run at once is set by $CFG->task_adhoc_concurrency_limit, which defaults to 3.
Give the database memory and less to scan
On MySQL or MariaDB, the InnoDB buffer pool decides whether queries read from memory or from disk. On a dedicated database server it should take most of the RAM. Then switch on the slow query log for a day and read it. A handful of queries usually account for most of the time, and they often come from one plugin or one report. Our article on database query optimization covers how to read that log.
Slow reports are mostly a data volume problem. The standard log table, mdl_logstore_standard_log, records every action and grows without limit unless you set a retention period. Find "Keep logs for" in the standard log store settings under Site administration > Plugins > Logging > Manage log stores, and choose a period that fits your record-keeping duties. For management reporting, use custom reports that read enrollment and completion data, schedule them to arrive by email overnight, and avoid site-wide log reports during teaching hours.
Prepare for a whole class starting a quiz at once
A timed quiz is the hardest load Moodle faces: hundreds of people click "Attempt quiz" in the same minute, and every page turn after that is a write. Three things help.
- Size the tiers together. PHP workers, database connections and Redis memory have to match. More PHP workers than the database accepts connections turns a slow exam into a failed one.
- Soften the start. Open the quiz a few minutes before the session and let each student's time limit run from their own start, so logins and first page loads are spread out.
- Put a handful of questions on each page. One question per page multiplies requests. Everything on one page makes each save large and loses more if a connection drops.
Then rehearse. Moodle includes generators for a test course and a JMeter test plan under Site administration > Development, meant for a staging site. Run the plan at the size of your largest exam and watch which tier fills first. Our guide to load testing before launch explains how to read the results.
What to do yourself and what to hand over
The checks inside Moodle are safe for any administrator: Performance info, the task screens, log retention, moving backups to the night. Redis, PHP-FPM limits and database memory are different. They need root access, a staging copy to test on, and someone who can read the results. If the site carries exams or paid courses, that is a reason to put it under a Moodle maintenance and support plan, or to ask for a one-off performance audit before the next busy period.
Frequently asked questions
Does Moodle need Redis?
No. A single server with a fast local disk runs well on the default file caches. Redis earns its place when moodledata is on network storage, when there is more than one web server, or when several hundred people are active at once.
How can I tell that cron is the problem?
The signs are delays, not slow pages: forum emails arrive hours late, completion does not update, certificates and badges appear the next day. Confirm it on the Scheduled tasks screen, where the "Last run" times show whether cron is running every minute, and in the task logs.
Why is Moodle slow only at certain times of day?
Either many people arrive together, such as the start of a class, or a heavy task runs at that time, such as automated backups or a large report. Compare the slow periods with the task logs and the server's CPU and memory graphs.
Will a bigger server fix a slow Moodle site?
Sometimes, and it is the most expensive first step. If OPcache is too small, caches sit on slow storage or one query scans millions of log rows, a bigger server hides the fault for a while. Measure and fix the configuration first, then add capacity for the load that remains.
Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.
