
I remember a particularly painful deployment for a university in Sydney. They had a substantial student body in Southeast Asia, and their Moodle instance, hosted on a respectable 64GB RAM AWS EC2 instance in us-east-1, was crawling. Login times were acceptable, but loading course pages, fetching static assets like images and JavaScript, and even just navigating between activities often exceeded 5-7 seconds for users in Manila or Jakarta. We were seeing TTFB (Time To First Byte) values of 800ms to 1.2s consistently, even for cached static content. It was a classic case of geographical latency choking an otherwise well-provisioned Moodle server. My mission was clear: drastically reduce this latency and improve overall perceived performance, and I immediately knew a robust moodle cdn setup using Cloudflare was the answer.
Moodle, at its heart, is a dynamic PHP application. While it has its own internal caching mechanisms (which are excellent for reducing database load and server processing), it doesn’t inherently solve the problem of network latency for geographically dispersed users. Every request for a theme file, an image from /pluginfile.php, or a JavaScript library still has to travel from the user’s browser all the way to your origin server and back. For our Sydney client, this round trip to Manila added ~150-200ms per request. Imagine a Moodle page with 50 static assets; that’s 7.5 to 10 seconds of pure network overhead just waiting for packets.
This is precisely where a Content Delivery Network (CDN) like Cloudflare becomes indispensable for a moodle cloudflare integration. Cloudflare positions copies of your static content on edge servers globally. When a student in Jakarta requests a Moodle image, Cloudflare serves it from its nearest data center – perhaps Singapore or Kuala Lumpur – rather than forcing the request to traverse the entire Pacific. This dramatically reduces latency and bandwidth usage on your origin server, translating to faster load times and a smoother user experience. I’ve personally seen page load times drop from 7 seconds to under 1.5 seconds for remote users after a proper Cloudflare integration.
Before you even touch Cloudflare, ensure your Moodle instance is configured to play nice with a CDN. This isn’t just about speed; it’s about correctness. I spent 8 hours once debugging broken file paths because I overlooked a critical setting.
Site administration > Server > HTTP. Ensure Use slash arguments is checked. This ensures Moodle generates clean URLs for file serving (e.g., /pluginfile.php/1/mod_resource/content/0/document.pdf instead of pluginfile.php?file=/1/mod_resource/content/0/document.pdf). Cloudflare caches these cleaner URLs more effectively.$CFG->cachejs = true; in config.php to combine and minify JavaScript files, further reducing the number of requests.lms.university.edu) through Cloudflare. In your Cloudflare DNS settings, create an A record pointing to your origin server’s IP address (or a CNAME if using another service like AWS ELB) and ensure the orange cloud is enabled. This directs all traffic through Cloudflare’s network.You also need to tell Moodle that it’s behind a reverse proxy. Add these lines to your config.php before the require_once(...) line:
// Cloudflare CDN / Reverse Proxy settings
$CFG->reverseproxy = true;
$CFG->reverseproxyremoteuser = false; // Set to true only if Cloudflare passes a remote user header
$CFG->sslproxy = true; // Use this if Cloudflare handles SSL for your domain
$CFG->cdnurl = 'https://lms.university.edu'; // Important: Use your actual Moodle domain
$CFG->wwwroot = 'https://lms.university.edu'; // Ensure this matches your CDN URL if behind Cloudflare
The $CFG->cdnurl and $CFG->wwwroot are critical. While $CFG->cdnurl might seem redundant if your entire site is proxied, it’s good practice. For a very specific setup where you might only CDN static content via a subdomain (e.g., cdn.lms.university.edu), $CFG->cdnurl would point there, while $CFG->wwwroot would be your main domain. For a full proxy via Cloudflare, they often align.
This is where the real magic happens for moodle cloudflare optimization. Moodle is highly dynamic, so you can’t just Cache Everything indiscriminately. Aggressive caching can break logins, assignment submissions, and other interactive elements. My approach involves using Page Rules for broad strokes and Cloudflare Workers for fine-grained control.
Cloudflare Page Rules (Priority-based, top-down):
I typically set up 3-5 page rules for Moodle:
Bypass Cache for Admin/Login/Dynamic Pages (Highest Priority):
*lms.university.edu*/(login|admin|my|message|user|report|grade|calendar|mod/quiz/attempt.php)*Cache Level: BypassCache Everything for Static Assets (Medium-High Priority):
*lms.university.edu*/(pluginfile.php|theme|pix|local|mod|course|moodle/plugin|repository|lib/yui|question|blocks|filter|lang|index).php*Cache Level: Cache EverythingEdge Cache TTL: 7 days (or 30 days for very static content)Browser Cache TTL: 1 day/pluginfile.php is particularly important as it serves user-uploaded files and course resources. I’ve set Edge Cache TTL to 7 days – it’s a good balance; too long and you risk serving stale content if an asset changes rapidly, too short and you lose the CDN benefit.Default Caching for Public-Facing Content (Lower Priority):
*lms.university.edu/*Cache Level: Standard (or No Query String if you want to be more specific)Edge Cache TTL: 2 hoursBrowser Cache TTL: 30 minutesStandard will respect your origin’s Cache-Control headers. If your Moodle installation serves content with Cache-Control: public, max-age=3600, Cloudflare will respect that. For any pages without explicit headers, it defaults to the Edge Cache TTL.Cloudflare Workers for Advanced Scenarios:
Sometimes, Page Rules aren’t enough. For instance, you might want to cache certain content for non-logged-in users but bypass the cache entirely for authenticated users. This is where Workers shine.
I often use a Worker script to inspect cookies. Moodle sets a MoodleSession cookie (or MOODLEID_ followed by a hash) when a user logs in. We can use this to bypass caching dynamically.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// Define paths that are ALWAYS bypassed (e.g., login, admin, messaging)
const bypassPaths = [
'/login/',
'/admin/',
'/my/',
'/message/',
'/user/',
'/report/',
'/grade/',
'/calendar/',
'/mod/quiz/attempt.php'
];
// Check if the request path matches any of the bypass paths
if (bypassPaths.some(path => url.pathname.startsWith(path))) {
return fetch(request); // Bypass cache and fetch directly from origin
}
// Identify Moodle session cookies (adjust MOODLEID_ to your Moodle's specific cookie prefix if different)
const cookies = request.headers.get('Cookie') || '';
const isMoodleLoggedIn = /(MoodleSession|MOODLEID_)/.test(cookies);
// If a Moodle session cookie is present, bypass cache for most pages
// EXCEPT specific static asset types that are safe to cache even for logged-in users.
const staticAssetExtensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.otf', '.eot', '.pdf', '.zip', '.mp4', '.webm'];
const isStaticAsset = staticAssetExtensions.some(ext => url.pathname.endsWith(ext)) || url.pathname.startsWith('/pluginfile.php');
if (isMoodleLoggedIn && !isStaticAsset) {
// For logged-in users, bypass cache for dynamic pages, but still cache static assets.
// We can also add a cache-control header to prevent browser caching for dynamic content.
const response = await fetch(request);
let newHeaders = new Headers(response.headers);
newHeaders.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
newHeaders.set('Pragma', 'no-cache');
newHeaders.set('Expires', '0');
return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders });
}
// Otherwise, attempt to serve from cache or fetch from origin
// Cloudflare's default caching behavior (based on Page Rules and origin headers) will apply
return fetch(request);
}
This Worker provides an extra layer of intelligent caching that Page Rules alone can’t quite replicate. It essentially allows authenticated users to still hit Cloudflare’s cache for truly static assets (like /pluginfile.php PDFs or theme images), while ensuring dynamic, personalized content is always fresh from the origin.
Implementing a CDN isn’t a “set it and forget it” task. You need to verify its effectiveness and diagnose issues. This is where my team often leverages tools like Grafana, combined with Cloudflare’s analytics, to build comprehensive dashboards. Building a Student Performance Dashboard with Grafana and Moodle Data taught us the power of centralized monitoring.
cf-cache-status: HIT or DYNAMIC (if not cached), and cf-ray which is useful for Cloudflare support. Cache-Control headers from your origin should also be visible.curl -I: From your terminal, use curl -I https://lms.university.edu/theme/yourtheme/pix/some_image.png. Check for cf-cache-status and Age headers. An Age header indicates how long the asset has been in Cloudflare’s cache.I’ve encountered my share of frustrating issues when integrating Moodle with Cloudflare. Here are a few common ones and their solutions:
Broken pluginfile.php Links for Specific Activities:
/pluginfile.php were showing as broken links or throwing 404s, even with the Page Rule set. I saw this particularly with custom SCORM packages or H5P content where the pluginfile.php URL often included complex query parameters.Cache Level: Cache Everything wasn’t actually caching URLs with complex query strings, or Moodle was generating slightly different URLs for the same asset. More critically, sometimes Moodle’s slasharguments setting wasn’t correctly applied everywhere, leading to ?file= syntax that was harder for Cloudflare to normalize.Use slash arguments is absolutely checked in Moodle’s HTTP settings and lib/setup.php allows it.pluginfile.php Page Rule, set Cache Level: Cache Everything and crucially, enable Query String: Standard or even Ignore Query String if you are certain query parameters don’t alter the file content (use with extreme caution!). For Moodle, usually Standard is safer.Bypass Cache on Cookie: MoodleSession to the Page Rule that covers pluginfile.php. This ensures that if a user has an active session, Cloudflare checks with the origin, preventing potential access issues for restricted files. It’s a trade-off but ensures correctness.Redirect Loops or Infinite Refresh on Login:
sslproxy setting in config.php is incorrect. Cloudflare might also be incorrectly trying to redirect HTTP to HTTPS, while your Moodle thinks it’s already on HTTPS. I once spent 4 hours going through Nginx logs, Moodle logs, and Cloudflare access logs before realizing the sslproxy setting was missing.sslproxy is set to true in config.php if Cloudflare handles SSL.wwwroot in config.php starts with https://.Bypass Cache Page Rule for login paths (e.g., *lms.university.edu*/login*) is active and correctly configured as the highest priority.Broken JavaScript or CSS After Clearing Moodle Cache:
php admin/cli/purge_caches.php), users would report broken layouts or JavaScript errors.Edge Cache TTL.*lms.university.edu*/theme/*) or, even better, purge everything. I usually trigger a full cache purge on Cloudflare immediately after any significant Moodle cache clearing or update. This is one thing I wish someone had told me early on: Moodle’s cache purge doesn’t magically tell Cloudflare to purge!Adopting a moodle cloudflare setup is a powerful move, but it’s not without its considerations.
Benefits:
Caveats & Recommendations:
Full (strict) SSL mode in Cloudflare for maximum security.MoodleSession cookies are non-negotiable for a robust moodle cdn setup.My clear recommendation: If you have a Moodle instance serving users across multiple geographic regions, or if your server is struggling under the load of static asset requests, implementing Cloudflare CDN caching is a mandatory step. The performance gains and reduced load on your origin server are invaluable. Just ensure you allocate sufficient time for proper configuration and thorough testing.
Optimizing Moodle performance is just one facet of managing a robust EdTech ecosystem. The principles of automation and data-driven insights apply across various LMS platforms and supporting services. For those managing other systems, consider how you might apply similar strategies, perhaps looking into solutions like Automating Canvas LMS Enrollments Using Python and REST APIs to streamline operational tasks. Furthermore, securing these environments is paramount, often achieved through centralized identity management; exploring options like Building a Campus-Wide Single Sign-On (SSO) with Keycloak can significantly enhance user experience and administrative control.