Why XOOPS relies on external cron
XOOPS was designed around a request-driven model. Almost everything it does happens while a visitor is loading a page: the preload system fires, modules render, and any pending work gets processed inline. There is no long-running daemon and no internal scheduler shipping with the core, which means anything that must run on a fixed clock, such as sending queued notification digests, pruning expired sessions, cleaning the template and smarty cache, or triggering a module's import routine, needs an outside process to knock on the door at the right time.
On a Managed Cloud Shared Hosting account running LiteSpeed on AlmaLinux and CloudLinux, that outside process is the cron daemon exposed through cPanel Jupiter (Advanced > Cron Jobs) or DirectAdmin Evolution (Advanced Features > Cron Jobs). You never touch the system crontab directly and you have no root, so every scheduled task is a line owned by your unprivileged hosting user. The job either runs the PHP CLI binary against a script inside your account, or it fetches a public URL that maps to a XOOPS endpoint. Both approaches work, but they behave very differently under CloudLinux resource limits, and choosing the wrong one is the most common reason a XOOPS site appears to "forget" to send mail or clear stale caches.
The practical consequence is that many XOOPS behaviors people assume are automatic are actually piggybacking on human traffic. A low-traffic site can go hours without a page view, during which no notification email leaves the queue and no expired cache entry is removed. Once you understand that timing gap, the fix becomes a matter of giving XOOPS a predictable heartbeat through the hosting panel rather than hoping a visitor arrives.
Choosing between PHP CLI and URL-based cron
The first decision is how the cron line reaches your code. On CloudLinux, PHP CLI processes and web (LSAPI) processes are counted differently, and the PHP version selected in the panel's MultiPHP Manager or PHP Selector applies to the web context. CLI may default to a different interpreter unless you call the versioned binary explicitly. Because XOOPS is sensitive to PHP version mismatches, the cleaner route on this platform is usually a URL-based hit that runs inside the same web environment your site already uses, guaranteeing the same PHP version, the same .user.ini limits, and the same paths.
Create a small dispatcher inside your document root so cron has a single, safe entry point. Using cPanel File Manager, navigate to /home/USERNAME/public_html/ and add a file named xcron.php:
<?php
// xcron.php - lightweight scheduled task runner for XOOPS
// Protect against random web hits with a shared token.
$expected = 'CHANGE_THIS_LONG_RANDOM_TOKEN';
if (php_sapi_name() !== 'cli') {
if (!isset($_GET['key']) || !hash_equals($expected, $_GET['key'])) {
header('HTTP/1.1 403 Forbidden');
exit('Forbidden');
}
}
define('XOOPS_ROOT_PATH', __DIR__);
include XOOPS_ROOT_PATH . '/mainfile.php';
// Example task 1: flush expired Smarty/template cache
$cacheDir = XOOPS_ROOT_PATH . '/uploads/caches/smarty_cache';
if (is_dir($cacheDir)) {
foreach (glob($cacheDir . '/*') as $f) {
if (is_file($f) && (time() - filemtime($f)) > 86400) {
@unlink($f);
}
}
}
// Example task 2: trigger a module's notification digest
// $notification_handler = xoops_getHandler('notification');
// ... module-specific dispatch here ...
echo "xcron completed " . date('c') . "\n";
The token check matters because this file sits in a publicly reachable path. Without it, anyone could hammer the endpoint and force repeated cache clears or mail dispatch, which becomes a self-inflicted resource problem on a shared account. The hash_equals comparison avoids timing leaks, and the php_sapi_name branch lets the same file run harmlessly from CLI later if you decide to switch.
Now register the cron in the panel. In cPanel Cron Jobs, add a common schedule such as every fifteen minutes and paste a command that fetches the URL quietly:
*/15 * * * * /usr/bin/curl -s "https://example.com/xcron.php?key=CHANGE_THIS_LONG_RANDOM_TOKEN" >/dev/null 2>&1If curl is unavailable, wget -q -O /dev/null is an equivalent fallback. In DirectAdmin the interface is nearly identical: fill the minute, hour, day, month, and weekday fields, then the command box. Keep the interval sane. Running every minute on a shared plan wastes your CloudLinux EP (entry process) and CPU budget for tasks that only need to run hourly.
If you prefer true CLI execution, call the versioned binary so PHP matches your site. On our platform that looks like /opt/alt/php82/usr/bin/php /home/USERNAME/public_html/xcron.php, substituting the PHP version you selected in the panel. CLI avoids web timeouts, which helps for heavier imports that might exceed the LiteSpeed request window.
Tuning limits for imports and heavy jobs
Background work that touches many database rows, such as an RSS import module, a mass mailer, or a bulk media reindex, will hit two ceilings: PHP execution time and memory. Because you cannot edit php.ini or my.cnf, the correct levers are the PHP Selector and a per-directory .user.ini. Place a .user.ini beside xcron.php in /home/USERNAME/public_html/ with modest, honest values:
max_execution_time = 120
memory_limit = 256M
max_input_time = 120These apply to the web-run cron because it uses LSAPI. Remember that LiteSpeed also enforces its own connection timeout, so a single URL hit that runs for minutes may be cut off even when PHP would allow it. The durable pattern for large jobs is chunking: have xcron.php process a fixed batch (for example 200 records) per invocation and record progress in a XOOPS config row or a small state file under uploads/. The next cron tick picks up where the last one stopped. This keeps every run short, stays within EP limits, and avoids the half-finished imports that leave XOOPS module tables inconsistent.
For notification-heavy sites, avoid dispatching hundreds of emails in one pass. Batch outbound mail so you do not trip the hourly sending limits that shared mail servers enforce, which would otherwise bounce the remainder of the queue. A batch of twenty to fifty per run, every fifteen minutes, drains a backlog smoothly without triggering rate protection.
Troubleshooting silent or failing tasks
When a scheduled task does not appear to run, resist changing the schedule first. Confirm the endpoint works by opening the URL manually in a browser with the correct key parameter. A blank page with your echoed completion line means the code path is healthy and the problem is the cron trigger; a 500 or partial output means the problem is inside XOOPS or PHP limits.
For PHP-level faults, check the error_log file that LiteSpeed writes into the directory where the script executes, typically /home/USERNAME/public_html/error_log, viewable in File Manager. Fatal errors about memory or missing classes surface here. If XOOPS debug output is needed, enable System Admin > Preferences > General Settings > PHP debug mode temporarily, reproduce the task by hitting the URL, then turn it off so debug notices are not exposed to visitors.
Cron delivery itself is easy to verify. Both cPanel and DirectAdmin let you set an email address that receives the output of every cron run; leave it populated during setup so you see curl errors, 403 token mismatches, or DNS resolution failures immediately. A frequent gotcha is the site being behind a redirect: if your canonical host is https://www.example.com but cron calls the bare domain, the request may return a 301 that curl does not follow, so the task never executes. Add -L to follow redirects or point cron at the exact canonical URL. Another is a maintenance or IP-restriction rule in .htaccess that blocks the request; whitelist the loopback or your server's own address, or better, exempt the xcron.php path so scheduled hits always pass. Once the endpoint answers cleanly and the panel confirms the schedule fired, XOOPS gains the steady heartbeat it never had on its own.