Moodle Restrict Access: Conditional Logic That Scales (Until It Doesn't)
Moodle's Restrict Access system enables differentiated learning paths without course duplication. This post covers how condition evaluation works, a phased rollout example, and the performance trade-offs that appear at scale.
18 Feb 2026, 11:00 UTC

The problem: one course, thirty different learning paths
You're running a mandatory compliance course for 500 employees across five departments. Finance needs the budgeting module by Friday. HR needs the harassment policy today. IT has already completed the security awareness unit last quarter. Everyone sees the same course page, but what they can do differs wildly.
Moodle's Restrict Access system solves this without duplicating courses or hacking permissions. It evaluates conditions per user at render time—date, grade, completion, group, profile field—and combines them with AND/OR logic. The thesis: it's the right abstraction for differentiated pacing, but the per-user evaluation model creates predictable scaling limits you need to plan for.
How Restrict Access evaluates conditions
When a user loads a course page, Moodle iterates through every activity and resource. For each item, it checks the restrict_access rules stored in the course_modules table (serialized as JSON since Moodle 3.11). Each condition type—date, grade, completion, group, profile—maps to a PHP class implementing \\core\\availability\\condition.
The evaluation is stateless per request. No cached result persists between page loads. This keeps logic simple: if a user's grade on Quiz A crosses 80%, the next topic appears immediately on refresh. But it also means a course with 200 activities and 500 concurrent users runs 100,000 condition evaluations per page view.
Conditions combine via a tree structure. A typical rule like \"Group = Finance AND (Date after 2026-10-01 OR Grade > 70% on Pre-test)\" becomes a nested array. Moodle evaluates bottom-up, short-circuiting where possible. Group and profile checks are fast (single table lookups). Grade and completion checks join grade_grades and course_completions tables respectively—indexed, but still I/O.
Worked example: phased rollout with fallback
Scenario: A 12-week onboarding course. Week 1–2 content opens immediately. Week 3–4 unlocks after completing Week 2 quiz with ≥70%. Week 5+ unlocks by calendar date (prevents rushing). Managers in the \"Team Leads\" group see everything from day one for coaching.
- Enable completion tracking: Site administration → Advanced features → Enable completion tracking.
- On each Week 1–2 activity: Activity completion → Completion tracking → \"Show activity as complete when conditions are met\" → Require view.
- On Week 2 quiz: Set grade to pass = 70 in quiz settings.
- On Week 3–4 section (or each activity): Restrict access → Add restriction → Grade → Select Week 2 quiz → ≥ 70%.
- On Week 5+ section: Restrict access → Add restriction → Date → From 2026-11-01.
- Add OR group: Restrict access → Add restriction → Group → Team Leads. Change the top-level combinator from AND to OR (click the \"Any of the following\" toggle).
Result: A regular user sees Week 1–2. After passing the quiz, Week 3–4 appears. On November 1, Week 5+ appears. A Team Lead sees all sections immediately because the group condition satisfies the OR branch.
Verify: Log in as a test student. Confirm Week 3–4 is hidden (shows greyed-out with \"Not available unless...\" message). Submit quiz with 75%. Refresh course page—Week 3–4 now visible. Log in as Team Lead—all sections visible.
Trade-offs: evaluation cost and UX surprises
Performance. Each condition type has a is_available() method. Grade conditions query grade_grades with userid, itemid, finalgrade. Completion conditions hit course_modules_completion. On a 300-activity course with 1,000 users, a single course index page can generate 300,000 queries if caching isn't tuned. Mitigation: enable the availability cache store (Moodle 4.0+) and consider splitting mega-courses into linked meta-courses.
Group mode interaction. Restrict Access by group and Group mode (Separate/Visible groups) are orthogonal. A user in Group A with a restriction \"Group = B\" sees the activity greyed out. But if the activity uses Separate groups, they also can't see Group B's posts. The greyed-out message won't explain the group mode conflict. Document this for instructors.
Hidden vs. greyed-out. The \"eye\" icon in Restrict Access toggles between \"hide entirely\" and \"show greyed-out with message.\" Hidden activities disappear from the course index, gradebook, and completion report for that user. Greyed-out activities remain in the gradebook (confusing if they're not yet accessible). Choose deliberately.
Profile field conditions. These check user_info_data against custom profile fields. Useful for \"Department = Finance\" rules. But profile fields are free-text unless you enforce a select menu. Typos (\"Finace\") silently break the condition. Validate on import or use cohort sync instead.
Verification checklist for your instance
- Create a test course with 5 activities. Set a date restriction on Activity 3 (tomorrow). Confirm it's greyed-out today, visible tomorrow.
- Add a grade restriction on Activity 4 (Activity 2 quiz ≥ 60%). Log in as student, fail quiz, confirm Activity 4 hidden. Regrade to 65%, refresh, confirm visible.
- Enable completion tracking. Set Activity 5 to auto-complete on view. View as student, check Completion report (Course → Reports → Activity completion) shows checkmark.
- Test group OR logic: create Group \"Admins\", add restriction \"Group = Admins\" with OR combinator. Log in as non-member—restricted. Add user to group—immediate access without cache clear.
- Load test: Use
ab -n 100 -c 10 https://yourmoodle/course/view.php?id=2on a course with 50 restricted activities. Monitor DB slow query log forgrade_gradesorcourse_modules_completionscans.
If the load test shows >200ms median response, enable the availability cache ($CFG->availability_cache = true; in config.php) and retest. For very large deployments, consider the filter_availability plugin (community) which precomputes visibility nightly.
Closing: use the lever, watch the fulcrum
Restrict Access is Moodle's most powerful native differentiation tool. It keeps logic in the UI, version-controlled in course backups, and portable across restores. The engineering decision—per-request evaluation with no persistent cache—prioritizes correctness over throughput. That's the right default. Your job is knowing when the course size × user count × condition complexity product crosses the threshold where you need caching, course splitting, or a scheduled pre-compute job. Start with the verification steps above. They'll tell you where your instance stands today.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.