How OctoberCMS actually schedules background work
OctoberCMS inherits its task system from Laravel, which means it does not register dozens of individual cron jobs the way older PHP applications did. Instead, everything funnels through a single command, php artisan schedule:run. That one command is meant to fire every minute. When it runs, October's internal scheduler wakes up, checks every registered task against its own cron expression, and decides which ones are due right now. A plugin might register a cache cleanup that runs hourly, a backup task that runs at 3 a.m., and a newsletter dispatch that runs every fifteen minutes. None of those get their own server cron entry. They all depend on that single minute-by-minute heartbeat.
This design is elegant on a dedicated server, but it trips up a lot of shared hosting users because the whole system is invisible until it fails. If the one cron line is missing, wrong, or pointed at the incorrect PHP binary, nothing throws a loud error. Scheduled backups simply never happen, import jobs sit untouched, and notification emails never leave. There is no red banner in the backend telling you the heartbeat stopped. The tasks were registered correctly; they just never got the trigger that evaluates them.
Core October features and many popular plugins lean on this. The system uses it to prune old backend log records, clean the temporary upload directory under storage/temp, and process the system_files cleanup. Plugins such as those handling scheduled content publishing, RainLab.Blog post scheduling, and any bulk import tool register their work here too. Because the scheduler is the single point of dependency, getting that one cron entry right fixes an entire class of "my automated task isn't running" complaints at once.
Setting up the single scheduler cron entry in cPanel and DirectAdmin
The correct cron entry runs schedule:run once per minute from your application root. The two things people get wrong are the working directory and the PHP binary, so both need explicit attention on CloudLinux, where the default php in cron often points at an old system version rather than the one your site uses.
In cPanel Jupiter, open Advanced > Cron Jobs. Under Common Settings, pick Once Per Minute (* * * * *), which populates all five time fields with an asterisk. In the Command field, enter the full path to the correct PHP interpreter followed by the absolute path to artisan. On CloudLinux the version-matched binary lives under /opt/alt/, and using the plain php command will frequently invoke a mismatched version that fails silently. A working command looks like this:
/usr/local/bin/ea-php82 /home/USERNAME/public_html/artisan schedule:run >> /dev/null 2>&1Replace USERNAME with your account username and adjust the PHP version to match what MultiPHP Manager reports for your domain. If your October install lives in a subdirectory or above the web root, point at that exact artisan path instead. To confirm the correct binary, open Software > Select PHP Version (PHP Selector) and note the active version; the ea-phpXX or /opt/alt/php-fpmXX path must correspond to it.
In DirectAdmin Evolution, the path is Advanced Features > Cron Jobs. Set minute to * and leave hour, day, month, and weekday as *. The command structure is identical. DirectAdmin accounts on CloudLinux commonly need the alt-php path:
/usr/local/php82/bin/php /home/USERNAME/domains/example.com/public_html/artisan schedule:run >> /dev/null 2>&1Once saved, verify it is actually being evaluated. In the backend, October's Settings > Administrators and log areas won't show scheduler health directly, so instead check the timestamp on files the scheduler touches, or run a quick manual test. Using the cPanel or DirectAdmin File Manager, open the Terminal only if your plan exposes it; if it does not, you can validate indirectly by installing a plugin that logs scheduled runs, or by watching storage/logs/system.log for scheduler activity. A missing or stale log timestamp after a couple of minutes usually means the PHP path is wrong.
Queue workers: why database is the only sane driver here
Some October tasks are queued rather than scheduled. Bulk imports, image processing, and notification sending often push jobs onto a queue so the web request returns quickly. On a dedicated server you would run php artisan queue:work as a persistent daemon supervised by systemd or Supervisor. That approach is off the table on shared hosting: you have no root, no systemctl, and any long-running process you start manually will be reaped by process limits. Do not attempt to keep a worker alive through SSH or a startup script; it will not survive and it violates the account's resource boundaries.
The workable pattern is to set the queue driver to database and process jobs in short bursts through the scheduler you already configured. First, confirm the driver in config/queue.php or the environment. Edit .env in the application root using File Manager and set:
QUEUE_CONNECTION=databaseAvoid redis and beanstalkd drivers, since those daemons are not available to your account. The sync driver is the fallback that runs jobs immediately inside the web request, which defeats the purpose of queuing and can cause timeouts on large imports. Database is the right middle ground: jobs land in a jobs table you can inspect through phpMyAdmin, and a scheduled command drains them.
To drain the queue without a daemon, register a bounded worker in your scheduler or add a second cron that runs queue:work with strict limits so it exits cleanly:
/usr/local/bin/ea-php82 /home/USERNAME/public_html/artisan queue:work --stop-when-empty --max-time=50 >> /dev/null 2>&1The --stop-when-empty flag tells the worker to process pending jobs and then quit, and --max-time=50 guarantees it never runs long enough to collide with the next minute's invocation. Schedule this once per minute alongside the main scheduler. This gives you near-real-time queue processing while respecting the platform's process and CPU limits.
Diagnosing silent failures and stuck jobs
When background work stops, the evidence lives in three predictable places. Start with storage/logs/system.log, readable through File Manager. Laravel-style stack traces land here, and a permissions error, a missing PHP extension, or an out-of-memory kill will be recorded with a timestamp you can match against when the task should have fired. If the file is empty or its modification time is hours old while your cron claims to run every minute, the cron is not executing the command at all, which points back to a wrong PHP path or a mistyped artisan location.
Next, open phpMyAdmin and inspect two tables. The jobs table holds pending queued work; rows piling up with an attempts value that keeps climbing mean the worker is picking jobs up but they throw on execution. The failed_jobs table captures the exception payload for jobs that exhausted their retries, and the exception column often names the exact class and line that broke. Clearing a stuck queue is as simple as deleting rows from jobs, but read the failure reason first so you fix the cause rather than just the symptom.
Memory and time limits cause a large share of import failures. Because cron-invoked PHP uses the CLI configuration rather than your web .user.ini, raising memory_limit only in the PHP Selector's web settings will not help command-line runs. Instead pass limits inline, for example ea-php82 -d memory_limit=256M artisan ..., keeping values inside your plan's ceiling. Finally, confirm the required extensions such as gd, curl, and zip are enabled in Select PHP Version for the version your cron binary uses, since the CLI and web contexts can end up with different extension sets and a missing one will abort image or archive jobs without touching the browser at all.