Vishal Tyagi
Exit/

Keep Laravel Queues Alive on Shared Hosting

5 min left

About this Codelab

What you'll build

A worker.js supervisor plus an optional /logs/workers route so you can see the queue breathe without SSH.

What you'll need

A Laravel app on shared hosting (or a local simulation), Node.js on the PATH, and permission to add a cron entry.

Duration: ~5 min6 stepsIntermediateUpdated 2024-08

Why cron alone fails

php artisan queue:work is a long-running loop. Cron starts a process and exits — so every minute you spawn a new worker that dies with the cron job, or you pile zombies. You need something that stays up and babysits PHP.

[!CHECKPOINT] Confirm the failure mode Run php artisan queue:work in an SSH session, disconnect, and confirm the worker dies. That pain is the requirement.

Write the supervisor

Create workers/worker.js next to (not inside) public/:

import { execa } from 'execa';
import fs from 'node:fs';
import path from 'node:path';

const config = {
  laravelPath: process.env.LARAVEL_PATH || path.resolve(import.meta.dirname, '..'),
  logFilePath: process.env.LOG_FILE_PATH || path.join(import.meta.dirname, 'worker.log'),
  queueWorkerCommand: process.env.QUEUE_WORKER_COMMAND || 'php artisan queue:work',
  restartDelay: Number(process.env.RESTART_DELAY || 1000),
};

function log(msg, isErr = false) {
  const line = `[${new Date().toISOString()}] ${msg}`;
  fs.appendFileSync(config.logFilePath, line + '\n');
  (isErr ? console.error : console.log)(line);
}

async function startQueueWorker() {
  log('Starting Laravel queue worker...');
  const [command, ...args] = config.queueWorkerCommand.split(' ');
  try {
    const subprocess = execa(command, args, { cwd: config.laravelPath });
    subprocess.stdout.on('data', (d) => log(d.toString().trim()));
    subprocess.stderr.on('data', (d) => log(d.toString().trim(), true));
    await subprocess;
  } catch (err) {
    log(String(err), true);
  }
  log('Worker exited. Restarting...');
  setTimeout(startQueueWorker, config.restartDelay);
}

startQueueWorker();

[!CHECKPOINT] Dry-run locally From workers/: LARAVEL_PATH=../ php worker.js (adjust) and dispatch a test job. You should see job output in worker.log.

Install deps and point env

cd workers
npm init -y
npm install execa

Set LARAVEL_PATH, LOG_FILE_PATH, and optionally QUEUE_WORKER_COMMAND=php artisan queue:work --tries=3.

Keep Node alive via cron

Many panels let Node keep running once started. A safe cron nudge (only useful if your host kills orphans):

* * * * * /usr/bin/pgrep -f 'workers/worker.js' >/dev/null || /usr/local/bin/node /home/YOU/workers/worker.js >>/dev/null 2>&1

Prefer starting once over SSH if the process persists.

Add a browser log tail (optional)

Route::get('/logs/workers', function () {
    $path = base_path('workers/worker.log');
    abort_unless(file_exists($path), 404, 'No worker log yet');
    return response(nl2br(e(file_get_contents($path))));
})->middleware('auth'); // lock this down

[!WARNING] Never expose worker logs publicly. Auth-gate or IP-restrict them.