How Invoice Ninja processes background work
Invoice Ninja (the Laravel-based v5 platform) pushes almost everything that isn't an immediate screen render into deferred processing. Recurring invoices that fire on a schedule, payment reminders, overdue notices, scheduled reports, PDF generation, email delivery, and data imports all run outside the normal web request. The application leans on two separate mechanisms that people frequently confuse: the scheduler and the queue.
The scheduler is Laravel's time-based dispatcher. It needs to run once per minute, and when it fires it checks which jobs are due — sending recurring invoices, generating reminders, cleaning temporary files, and so on. On a server with shell access this is driven by a single system cron line calling artisan schedule:run. The queue is a different animal: it holds individual tasks (one email, one PDF, one webhook) that the scheduler and the web app enqueue, and a worker drains that backlog. If the scheduler never runs, nothing gets enqueued on time. If the queue never drains, work piles up and emails never leave.
On managed shared hosting you have no root, no systemctl, and no ability to run a persistent queue:work daemon that stays alive forever. That constraint shapes the entire configuration. The practical approach is to drive the scheduler from the control panel's Cron Jobs feature and to run the queue in sync mode or as a short, cron-triggered queue:work --stop-when-empty pass rather than a long-lived daemon. Understanding which of the two is misbehaving is the first diagnostic step: a customer complaining that "recurring invoices didn't send" is almost always a scheduler problem, while "invoices generated but no email arrived" is almost always a queue or mail driver problem.
Configuring the scheduler cron in cPanel and DirectAdmin
The scheduler is a single cron entry that must run every minute. First, find the absolute path to your installation and the correct PHP binary. In cPanel Jupiter, open MultiPHP Manager to confirm the PHP version assigned to the domain — Invoice Ninja v5 needs PHP 8.1 or newer. Because the system php in cron may differ from the version serving your site, call the CloudLinux PHP selector binary explicitly. A reliable path pattern on CloudLinux is /opt/alt/php82/usr/bin/php (adjust the version number to match MultiPHP).
In cPanel go to Advanced → Cron Jobs. Set the timing fields to * * * * * (every minute) and enter a command like this, replacing the username and path with yours:
/opt/alt/php82/usr/bin/php /home/USERNAME/invoiceninja/artisan schedule:run >> /home/USERNAME/invoiceninja/storage/logs/cron.log 2>&1In DirectAdmin Evolution the equivalent lives under Advanced Features → Cron Jobs. The field layout is identical — five time fields plus the command. Use the same every-minute pattern. Redirecting output to a log inside storage/logs is deliberate: it gives you a place to read failures without shell access, since you can open that file later in the File Manager.
A frequent mistake is pointing the cron at public/index.php or at a URL. The scheduler is a console command and must target artisan in the application root, not the web root. If your document root is public_html and the application sits one level up (a recommended layout), the artisan file is outside the web-accessible directory — that is correct and intended. If your host restricts cron frequency, confirm in the panel that one-minute intervals are allowed; some plans throttle to every five minutes, which still works for Invoice Ninja but delays time-sensitive reminders.
After saving, wait two minutes and check File Manager → /home/USERNAME/invoiceninja/storage/logs/cron.log. An empty or steadily appending log with no PHP fatal errors means the scheduler is alive. You can also confirm inside the app under Settings → Account Management, where the dashboard surfaces warnings if the scheduler has not checked in recently.
Draining the queue without a persistent daemon
With no ability to keep a worker process running, you have two supported options. The simplest is to set the queue driver to sync, which executes each job inside the web request that created it. Open the application's .env file through File Manager (it lives in the install root, not public_html) and set:
QUEUE_CONNECTION=syncSync mode removes the need for a separate worker entirely — emails and PDFs are generated as the action happens. The tradeoff is that a slow mail server or a large PDF can make a page request feel sluggish, and a request timeout can abandon the job. For low-to-moderate invoice volume on shared hosting, sync is the most reliable choice because it has no moving parts to stall.
If you process enough volume that sync causes timeouts, switch to a database-backed queue drained by cron. In .env set QUEUE_CONNECTION=database, then add a second cron job that runs a bounded worker pass. Running it every minute with a stop condition keeps it from becoming an orphaned long-lived process:
/opt/alt/php82/usr/bin/php /home/USERNAME/invoiceninja/artisan queue:work --stop-when-empty --tries=3 --max-time=55 >> /home/USERNAME/invoiceninja/storage/logs/queue.log 2>&1The --stop-when-empty and --max-time=55 flags guarantee the process exits before the next minute's cron fires, so you never stack overlapping workers and exhaust the account's process limit — a common cause of CloudLinux LVE "resource limit reached" errors. The --tries=3 flag retries transient failures rather than dumping them straight into the failed jobs table.
Troubleshooting stuck and silent jobs
When recurring invoices stop generating, work through the chain in order. Confirm the scheduler cron actually ran by checking the timestamp and contents of storage/logs/cron.log. A permission denied error there usually means the PHP binary path is wrong or the storage directory is not writable — in File Manager, verify storage and bootstrap/cache are set to 0755 with files at 0644, owned by your account.
If the scheduler runs cleanly but invoices still don't send, inspect storage/logs/laravel.log for exceptions. Database queue entries that never process show up in the jobs table; repeatedly failed ones land in failed_jobs. You can view both through phpMyAdmin — a growing jobs table with old timestamps proves the worker cron isn't draining, while rows in failed_jobs contain an exception column that names the real error, frequently an SMTP authentication rejection or a memory exhaustion from PDF rendering.
Memory limits are a recurring culprit. PDF generation and large imports can exceed the default PHP memory ceiling. Raise it per-directory with a .user.ini file in the application root (PHP reads it for the FastCGI/LiteSpeed handler):
memory_limit = 512M
max_execution_time = 120Allow a few minutes for LiteSpeed to pick up the new .user.ini values, or recycle PHP through MultiPHP Manager. For imports specifically, Invoice Ninja queues the parse job, so if the queue isn't draining the import appears to hang forever with no error — always confirm queue processing before assuming the import file is malformed.
Finally, timezone mismatches make correctly configured schedulers look broken. Set the account timezone under Settings → Localization in the Invoice Ninja AdminCP so that "send at 9 AM" matches your expectation rather than the server's UTC clock. Between an every-minute scheduler cron, a bounded queue worker or sync mode, correct file permissions, and sensible memory limits, every background task Invoice Ninja depends on will run predictably within the limits of a shared account.