Skip to content
Engineering notes

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:

  1. Set a reasonable max-time or max-jobs as a safety net (default for everything).
  2. Use shouldQuit inside specific jobs that you know are memory-heavy.
  3. Run queue:restart on a cron as a last-resort backstop if you are not using --max-jobs or --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:restart via cron keep things clean.
  • --max-jobs and --max-time let you cap worker lifespan without external tools.
  • shouldQuit from 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.