Most WebProCMS installs run background work inline — deferred closures after the response is sent, plus Lazy Cron for scheduled maintenance — because shared hosts rarely run a persistent queue:work process. But installs that do run a worker (a VPS with Supervisor, an owner-managed server) had no way to see it: is the worker alive, is the queue backing up, did anything fail? The Background Tasks screen (Dashboard → Develop → Background Tasks, admin and up) answers all three, and degrades gracefully on worker-less installs instead of alarming about a setup that's perfectly normal.
What the screen shows
| Section | What it tells you |
|---|---|
| Queue worker | The active queue connection/driver and a liveness verdict: Active (heartbeat within 5 minutes), Stale (a worker ran before but has gone quiet), Not detected (never seen — the normal fleet state), or Sync driver (jobs run inline, nothing to monitor). Shows the last heartbeat time and the last job a worker processed. |
| Test queue worker | A probe button (hidden on the sync driver): dispatches a trivial job onto the real queue and watches for a worker to pick it up. Confirms end-to-end liveness in seconds, or times out after 15 s — and then sweeps the probe job back off the database queue so worker-less installs don't accumulate junk rows. |
| Queue stats | Pending / in-flight / failed counts and the oldest pending job's wait time, per the driver-agnostic size-inspection API (works on database and Redis queues). A warning callout appears when jobs are pending but no active worker is draining them. |
| Queued jobs | The first 25 rows of the database queue: job class, queue name, age, attempts, waiting vs. processing. |
| Failed jobs | The 25 most recent failed_jobs rows with the exception's first line. Per-row Retry (pushes it back onto the queue) and Delete, plus Retry all / Delete all (both deletes are modal-confirmed). |
| Long-running tasks | Progress bars for any work that self-reports through BackgroundProgress (see below), plus an in-flight AI site-generation run. |
The page live-refreshes every 10 seconds while open.
How worker detection works
There is no reliable "is a process running?" API on shared hosting (exec is usually disabled), so detection is heartbeat-based (QueueWorkerMonitor, registered in AppServiceProvider):
queue:work's loop event fires every poll cycle — even when idle — and stamps a cache timestamp (throttled to one write per 30 s).- Every real queued-job execution stamps it too, and records the job's class name.
- The
syncconnection is deliberately ignored: sync jobs run inline in the dispatching request, and jobs run viadefer()never fire queue events — so neither can fake a live worker on an install that has none.
The heartbeat lives in the cache (database-backed by default), so a CLI worker and the FPM dashboard share state, including across servers with a shared cache backend.
Self-reported progress (BackgroundProgress)
Queue tables only know pending/reserved/failed — a "63% done" bar requires the work to report its own progress. BackgroundProgress is the one-line-per-milestone registry for that:
BackgroundProgress::start('media-cloud-migration', 'Migrating media to cloud', total: $count);
BackgroundProgress::increment('media-cloud-migration'); // per item
BackgroundProgress::complete('media-cloud-migration'); // or ::fail($key, $error)
Entries live in cache and age out on their own (6 h after the last update), so a crashed job's bar disappears instead of sticking forever. Any job, deferred closure, or artisan command can adopt it incrementally.
Setup
Nothing to configure. Installs without a worker see an informational note (that's the supported default — Lazy Cron and deferred execution handle background work). To run a real worker, start php artisan queue:work under a process monitor such as Supervisor and set QUEUE_CONNECTION=database (or redis) in .env; the screen will show it as Active within seconds of its first poll.
What the heartbeat unlocks
Worker detection isn't just informational — it gates real dispatch decisions:
- Lazy Cron queue offload (opt-in, Settings → Advanced → Lazy Cron): due scheduled tasks are handed to the worker instead of running inline, with automatic rescheduling if a dispatch is lost. See Lazy Cron → Queue worker offload.
- Worker-optional job sites (AI site generation, theme switching) use
QueueDispatch::dispatchOrDefer(): they dispatch to the queue only when a fresh heartbeat proves a worker is draining it, and otherwise run the work after the response viadefer()— replacing the old "an async driver implies a worker" guess with evidence.
Limitations
- Per-queue row listing covers the database driver; other drivers get aggregate counts for their default queue only.
- Progress bars only appear for work that calls
BackgroundProgress(or the AI site generator, which has its own run state) — adoption across long-running jobs is incremental. - Scheduled maintenance tasks are intentionally not duplicated here — they live on the Lazy Cron panel (Dashboard → Settings → Advanced), which the screen links to.