Kennedy Mutisya
Avoiding Memory Leaks When Running Laravel Queue Workers
Practical restart strategies for Laravel queue workers using cron, max options, custom quits, and Horizon.
There is an old debate about whether PHP is suitable for long-running processes. Having run hundreds of workers on large-scale projects, I can say PHP is perfectly capable, as long as you handle memory correctly.
The problem is not the language. It is that references pile up in memory that PHP's garbage collector cannot detect. The process grows, and eventually the server runs out of memory and crashes.
The fix is simple: restart your workers more often.
Restart Workers with Cron
With a process manager like Supervisor in place, you can restart workers every hour and let Supervisor bring them back automatically.
Drop this into your crontab:
0 * * * * forge php /home/forge/laravel.com/artisan queue:restart
This runs queue:restart every hour. All running workers receive the signal and gracefully exit after finishing whatever job they are currently processing. Supervisor detects the exit and spawns a fresh, clean process.
Built-in Options: max-jobs and max-time
If you would rather not use a cron job, Laravel gives you two command-line options that do the same thing:
php artisan queue:work --max-jobs=1000 --max-time=3600
--max-jobs: the worker exits after processing this many jobs, regardless of how long it took.--max-time: the worker exits after this many seconds, regardless of how many jobs it ran.
The check happens between jobs, not in the middle of one. So if a job is still running when the limit expires, the worker finishes it first and then exits.
The combination is useful: process up to 1,000 jobs or run for up to an hour, whichever comes first. This caps both memory growth and total runtime in a single command.
Signal the Worker from Inside a Job
Sometimes you know a specific job type is memory-heavy: maybe it processes a large file, batches thousands of database records, or loads big datasets into memory. You can tell the worker to quit right after that job finishes, from within the job's handle() method:
public function handle()
{
// Run the job logic.
app('queue.worker')->shouldQuit = 1;
}
This sets the shouldQuit flag that the worker checks at the end of each loop iteration. When the flag is true, the worker exits after the current job completes. This gives you fine-grained control: only restart after the jobs that actually need it, rather than on a fixed schedule.
Using Horizon
If you are using Laravel Horizon, you can configure maxJobs and maxTime directly in your supervisor configuration instead of passing CLI options:
'environments' => [
'production' => [
'supervisor-1' => [
// ...
'maxTime' => 3600,
'maxJobs' => 1000,
],
],
],
Horizon handles the rest. It manages the underlying worker processes and restarts them according to your limits.
The Strategy
Restarting workers is not a hack. It is the intended escape hatch for a fundamental constraint: any long-running process accumulates memory over time, no matter what language it is written in. The strategy is:
- Set a reasonable max-time or max-jobs as a safety net (default for everything).
- Use
shouldQuitinside specific jobs that you know are memory-heavy. - Run
queue:restarton a cron as a last-resort backstop if you are not using--max-jobsor--max-time.
The three approaches are complementary. Use the combination that matches your workload. A worker processing small, predictable jobs can run for hours. One handling large file uploads or API batch operations should cycle more frequently.
Key Takeaways
- PHP workers can leak memory over time: not a language flaw, just reality for any long-running process.
- Restart often: hourly restarts with
queue:restartvia cron keep things clean. --max-jobsand--max-timelet you cap worker lifespan without external tools.shouldQuitfrom inside a job gives you per-job-type control over restarts.- Horizon has built-in maxJobs/maxTime settings so you enforce the same rules without extra scripts.
- Do not fight the restart: it is how queue workers stay healthy. Embrace the cycle.