skip to content

After deploying a Laravel video-transcoding app, queue workers still run the old code; why, and how do queue:restart and Supervisor fix it?

level: middleimportance: must knowfreq 56%

answer

  1. long-lived process, code loaded once
  2. illuminate:queue:restart in the cache
  3. finish current job, then exit
  4. Supervisor autorestart and stopwaitsecs
  5. --max-jobs, --max-time, --memory recycle

basics

~20 s

queue:work loaded the old code at start and never reloads it. php artisan queue:restart writes a timestamp to the cache; each worker sees it, finishes its current job and exits, and Supervisor starts a fresh one.

solid answer

~50 s

A `queue:work` process boots the application once, so a deploy that swaps files does not change the code in memory. `php artisan queue:restart` stores the current time forever under the cache key `illuminate:queue:restart`. Each worker read that value when it started and compares it before taking the next job and while idle; when it differs, the worker finishes the job it is on and exits cleanly. Nothing in Laravel starts it again, so a process manager must: Supervisor with `autorestart=true` launches a new `queue:work` on the new release. Two conditions make this work: every worker must read the **same cache store** the deploy writes to, and Supervisor's `stopwaitsecs` must exceed the longest transcode so a stop does not kill a job midway. `--max-jobs`, `--max-time` and `--memory` also make workers exit regularly, which caps memory growth.

code

ini · 11 lines
ini
[program:transcode-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/current/artisan queue:work redis-long --queue=transcode --timeout=1700 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/app/shared/transcode-worker.log
stopwaitsecs=1800

go deeper

for a junior

Recall that workers must be restarted after a deploy and that php artisan queue:restart plus a process manager does it.

for a middle

Explain the cache key, the finish-then-exit behaviour and why Supervisor's autorestart is required for workers to come back.

for a senior

Diagnose missed restarts across hosts, size stopwaitsecs for long jobs, and add --max-time or --max-jobs recycling.

for a principal

Own the release process for workers: payload compatibility across versions, restart timing for long jobs, and who supervises processes.

## Why the old code keeps running A deploy of the transcoding app replaces files on disk, runs migrations and clears caches. A `php artisan queue:work` process started yesterday has already loaded the job classes, the config and the service providers into memory, and PHP does not reload a class once it is defined. So after the deploy: - `TranscodeVideo` jobs run with the old `handle()` logic; - a new job class that the old code does not know fails to unserialize; - a changed constructor signature breaks jobs queued by the new web code; - config changes, such as a new storage disk for renditions, are invisible. The docs state it directly: queue workers "will not notice changes to your code without being restarted". ## What queue:restart does `php artisan queue:restart` does not signal any process. It writes the current Unix time, forever, to the **default cache store** under `illuminate:queue:restart`. The worker side: 1. on start, `queue:work` reads that key and remembers the value; 2. after each job, and on each idle poll, it checks its stop conditions, including whether the key now holds a different value; 3. if it does, the worker exits with status 0 once the current job has finished, so no job is abandoned; 4. an idle worker notices within one `--sleep` interval (3 seconds by default). Because it is only a cache value, it has prerequisites: - the deploy host and every worker host must share one cache store — the skeleton's `CACHE_STORE=database` or Redis works; `array`, or a `file` store on separate hosts, does not; - `Queue::withoutInterruptionPolling()` or `Worker::$restartable = false` turns the check off, and then `queue:restart` reports that restarting is disabled. ## Who starts the new worker The worker has exited; Laravel does not respawn it. A **process manager** does. The docs use Supervisor: | Directive | Why it matters here | |---|---| | `command=php /path/artisan queue:work ...` | the worker and its flags; point `/path` at the current release | | `numprocs=4` | how many workers run | | `autorestart=true` | respawns a worker after `queue:restart`, a crash or a timeout | | `stopwaitsecs=3600` | how long `supervisorctl stop` waits before killing; must exceed the longest job | | `stopasgroup` / `killasgroup` | signals reach child processes such as an encoder | With a transcode that can take 25 minutes, a `stopwaitsecs` of 60 means stopping Supervisor kills encodes halfway through. `queue:restart` itself never kills a job; it waits for the job to finish, so a restart can take as long as the longest running job. ## Diagnosing a missed restart When some workers still run old code after a deploy: 1. Check the worker's start time with the process manager (`supervisorctl status`); a worker older than the deploy never exited. 2. Read `illuminate:queue:restart` from the cache the workers use and compare it with the deploy time; a missing or old value means the signal went to another store. 3. Check whether the app disables interruption polling in a service provider. 4. Check for a long job: a worker in the middle of a 25-minute encode exits only after it. 5. Check that the process manager restarts exited workers, and that its `command` points at the current release path rather than an old release directory. ## Recycling as a second line Even without deploys, long-lived workers should exit now and then: - `--max-jobs=500` exits after 500 jobs; - `--max-time=3600` exits after an hour; - `--memory=256` exits after a job if the process's allocated memory has reached 256 MB (default 128), with exit code 12. Supervisor restarts each of them, which caps slow memory growth from image libraries or accumulated static state and limits how long a missed restart can leave old code running. ## Putting it together A safe sequence for the transcoding app is: ship the new release, run `php artisan queue:restart`, and let Supervisor bring up fresh workers while old ones finish their current encodes. Keep job payloads compatible across one release, because jobs queued by the old code are processed by the new code and vice versa during the overlap. Where exactly `queue:restart` sits in a zero-downtime deploy script is a deployment concern; the mechanism above is what makes it work.

  • queue:restart ran on the web server, but workers on a separate host kept old code; what is the likely cause?
    The restart signal is a value in the default cache store. If the two hosts do not share that store — a `file` or `array` cache, or different `CACHE_STORE` settings — the workers never see the new timestamp. Point both at the same database or Redis cache, or run `queue:restart` where the workers' cache lives.
  • Why must Supervisor's stopwaitsecs exceed the longest transcode job?
    When Supervisor stops or restarts the program, it sends a stop signal and waits `stopwaitsecs` before killing the process. A worker that receives SIGTERM finishes its current job first, so if the wait is shorter than the encode, Supervisor kills it mid-job and the job is retried after `retry_after`.

saying these in an interview costs you the question

  • queue:restart kills running workers immediately, aborting their current jobs
  • Workers reload changed PHP files automatically at the next job
  • queue:restart starts new worker processes by itself
  • A file cache store works for queue:restart across several servers
  • --memory makes the worker stop in the middle of a job when the limit is hit