Cron + queue worker
KodMail needs one cron entry (Laravel scheduler) and one queue worker (background jobs). Without these, scheduled campaigns won't send, bounces won't process, and AI usage won't roll up.
Cron — Laravel scheduler
Single cron entry runs schedule:run every minute. Laravel internally dispatches the 11 scheduled tasks at their configured cadence:
* * * * * cd /var/www/kodmail && php artisan schedule:run >> /dev/null 2>&1
What runs on the schedule
| Cadence | Command | Purpose |
|---|---|---|
| Every min | campaigns:dispatch | Send queued campaign chunks. |
| Every min | campaigns:reconcile | Heal stuck campaign statuses. |
| Every min | automations:tick | Advance subscriber enrollments through workflow nodes. |
| Every min | webhooks:deliver | Outbound webhook retry queue. |
| Every min | system:heartbeat | Liveness signal for the setup-health dashboard. |
| Every 5 min | bounces:process | IMAP poll for bounce + complaint messages. |
| Hourly | ai:aggregate-usage | Roll up AI token + cost counters. |
| Daily 03:00 | send-time:recompute | Per-subscriber best-time-to-send re-calc. |
| Daily 03:15 | billing:reconcile | Self-heal Stripe vs DB state. |
| Daily 07:00 | notifications:scan-billing | Fire billing.plan_expiring at the configured lead days. |
| Weekly Mon 08:00 | digest:business | Owner-only weekly business digest email. |
Queue worker
Long-running process that drains jobs table. Required for: large campaign sends, CSV imports, async notification dispatch (when v1.1 lands).
Path A — Supervisor (VPS / dedicated)
; /etc/supervisor/conf.d/kodmail-worker.conf
[program:kodmail-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/kodmail/artisan queue:work --queue=campaigns,imports,default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/kodmail-worker.log
stopwaitsecs=3600
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start kodmail-worker:*
Path B — systemd service
# /etc/systemd/system/kodmail-worker.service
[Unit]
Description=KodMail queue worker
After=network.target mysql.service
[Service]
User=www-data
Group=www-data
Restart=always
WorkingDirectory=/var/www/kodmail
ExecStart=/usr/bin/php artisan queue:work --queue=campaigns,imports,default --sleep=3 --tries=3 --max-time=3600
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now kodmail-worker
Path C — Shared hosting (no Supervisor)
cPanel installs typically can't run long-lived workers. KodMail's scheduler dispatches a one-off queue:work --once every minute as part of schedule:run. Throughput is limited (~60 jobs/min) but works on any host with a single cron entry.
Local dev
Three terminals:
# Terminal 1 — web server
laragon start (or `php artisan serve`)
# Terminal 2 — queue worker
php artisan queue:work --queue=campaigns,imports,default
# Terminal 3 — scheduler (replaces cron locally)
php artisan schedule:work
Setup health dashboard
Admin → Settings → System → Setup health shows live tiles:
- Scheduler last heartbeat (should be within the last 90 s).
- Queue worker liveness (Horizon-style polling).
- Reverb (real-time) connection state.
- Per-job throughput + retry counts.
Green = all good. Yellow = stale heartbeat (cron / worker stopped). Red = error / crash.
documentation/screenshots/setup-health.png/admin/settings/system-setup — live infrastructure tiles + install snippets.