Every file Moodle serves goes through PHP by default. On sites with large video files or a high number of concurrent downloads, this ties up PHP-FPM workers for the duration of each transfer. Enabling X-Sendfile transfers that work to the web server, freeing PHP instantly.

Why PHP file serving is a bottleneck

When a user accesses a file in Moodle — a video, a PDF, a SCORM package — the request goes to pluginfile.php. Moodle checks permissions, resolves the file from moodledata, and streams it back to the browser. While the download is in progress the PHP-FPM worker is occupied: it cannot serve other requests.

A single 500 MB video streamed to a user on a slow connection can hold a PHP worker for 10–20 minutes. On a site with a limited FPM pool, a handful of concurrent video viewers can exhaust all available workers, making the entire site unresponsive.

How X-Sendfile works

X-Sendfile is a mechanism where PHP sets a response header instead of sending the file body. The web server intercepts that header, locates the file on disk, and streams it directly to the client. PHP exits immediately — the FPM worker is released.

The header name differs by web server: – Apache: X-Sendfile (requires mod_xsendfile) – Nginx: X-Accel-Redirect

Moodle has built-in support for both.

Apache configuration

Install mod_xsendfile if it is not already present:

apt install libapache2-mod-xsendfile
a2enmod xsendfile

In your virtual host configuration, declare the paths that Apache is allowed to serve via X-Sendfile:

XSendFile On
XSendFilePath /var/moodledata
XSendFilePath /var/www/moodle

The path must match the real filesystem path of moodledata and, if your theme or plugin serves files from the Moodle root, the Moodle directory as well.

Nginx configuration

Add an internal location block that maps to your moodledata directory:

location /moodledata-internal/ {
    internal;
    alias /var/moodledata/;
}

The location name is arbitrary — it just needs to match what Moodle will put in the header.

Moodle configuration

In config.php, add:

// Apache:
$CFG->xsendfile = 'X-Sendfile';
$CFG->xsendfilepath = '/var/moodledata';

// Nginx:
$CFG->xsendfile = 'X-Accel-Redirect';
$CFG->xsendfilepath = '/moodledata-internal';

For Nginx the xsendfilepath value is the location block name (the internal URI), not the real filesystem path.

Verifying it works

After enabling, inspect the response headers on a file download:

curl -I "https://yourmoodle.com/pluginfile.php/1/course/section/0/file.pdf" \
  -b "MoodleSession=yoursessioncookie"

Without X-Sendfile the response body comes directly from PHP. With X-Sendfile the PHP response will have Content-Length: 0 and the X-Sendfile header will be present (stripped by the web server before sending to the client, but visible in debug mode).

A simpler check: watch FPM worker utilization during a large file download with watch -n1 'php-fpm8.2 -t 2>&1; ps aux | grep php-fpm | grep -v grep | wc -l'. Without X-Sendfile the count stays elevated for the duration of the download. With X-Sendfile the worker count drops back to baseline within seconds.

Caveats

moodledata must not be web-accessible directly. X-Sendfile only works because Moodle’s PHP code runs first to enforce access control. The web server then serves the file after PHP has authorised it. Never point a public Alias or root directly at moodledata.

Symlinks: if moodledata contains symlinked directories (some backup or filedir configurations do this), ensure mod_xsendfile or Nginx is configured to follow them, or the file lookup will fail.

PHP memory: X-Sendfile also eliminates the memory overhead of reading the file into PHP before sending. This is particularly relevant for ZIP files or large SCORM packages where PHP would otherwise buffer the entire file.

Solin provides Moodle performance tuning, hosting, and infrastructure consulting.

Contact us