Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series.
People love to argue about whether PHP is suited to long running processes. In practice, Laravel workers crunch enormous numbers of jobs reliably on modest hardware. But because a worker keeps one PHP process alive for a long time, small amounts of memory can pile up that PHP never reclaims, and left unchecked that can eventually exhaust the server. The fix is not to chase every leak. It is simply to restart workers often.
Why a long lived process leaks
A normal web request starts, does its work, and dies, taking all its memory with it. A worker does not. It boots once and runs for hours or days, and over thousands of jobs, references accumulate that PHP's garbage collector cannot always free. Eventually memory creeps up. Rather than hunt down every cause, you accept it and recycle the process regularly. A fresh worker starts clean.
Option 1: max-jobs and max-time
The built in way is to tell the worker to exit after a number of jobs or a stretch of time, and let your process manager start a fresh one.
php artisan queue:work --max-jobs=1000 --max-time=3600
This worker exits after 1000 jobs or 3600 seconds, whichever comes first. The check happens between jobs, so a worker never quits in the middle of processing one. Supervisor (or systemd, or Horizon) immediately starts a replacement, and the memory is freed.
Option 2: a scheduled queue:restart
If you would rather recycle on a clock, run queue:restart on a schedule. It signals every worker to finish its current job and exit, and the process manager brings them back.
0 * * * * php /home/forge/site.com/artisan queue:restart
That cron line restarts workers every hour. It is the same graceful signal used on deploy, so no job is interrupted.
Option 3: quit from inside a heavy job
Some specific jobs are known memory hogs. After one of those runs, you may want the worker to restart immediately to release what that job consumed, rather than waiting for the next threshold.
public function handle(): void
{
// ... heavy work
app('queue.worker')->shouldQuit = 1; // exit after this job
}
The worker finishes the job, then exits cleanly, and the process manager restarts it fresh.
With Horizon
Horizon manages workers for you, so configure the same limits in your supervisor block instead of on the command line.
'environments' => [
'production' => [
'supervisor-1' => [
// ...
'maxJobs' => 1000,
'maxTime' => 3600,
],
],
],
Horizon then recycles its workers on those limits automatically. You can also restart the whole Horizon process on a schedule with horizon:terminate if you prefer a time based recycle.
Wrapping up
Worker memory growth is normal for a long running PHP process, and the cure is regular restarts, not leak hunting. Use --max-jobs and --max-time (or Horizon's maxJobs and maxTime), or a scheduled queue:restart, and quit from inside a job with shouldQuit for known-heavy work. A process manager always brings a fresh worker back, so recycling costs you nothing and keeps memory flat.
More in the series: rationing worker memory and CPU and Laravel queues and deployments. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.