Laravel Horizon and the maxProcesses trap
By Daniel Samson · 2026-03-23
I replaced a pile of hand-managed queue:work processes with Laravel Horizon. Mostly a clear win — but the default configuration quietly tried to eat my cluster's memory, and it took an OOMKill or two to work out why.
Why Horizon over raw workers
One supervisor config instead of a supervisord file per queue. Auto-balancing worker pools. A real-time dashboard showing throughput, wait times and failures. Sane failed-job retry. Compared to babysitting individual worker processes, it's a genuine upgrade in operability.
The maxProcesses gotcha
Here's the thing nobody warns you about: every Horizon worker process is a full PHP process with your entire application booted into memory. The default maxProcesses of 10 means ten complete copies of your app, per pod. On a media pipeline where each job is already heavy, that's gigabytes. I had to drop the production default-worker maxProcesses from 10 to 3 and raise the pod memory limit to 1.5 GB before the OOMKills stopped.
Right-size per queue
The real fix is per-queue balancing: a small number of processes for the heavy queues (video conversion), more for the cheap ones (notifications). Don't let a single global maxProcesses fan the same fat number out across every queue you have.
Don't forget phpredis
Horizon leans on Redis hard. The default predis client is pure PHP and noticeably slow under load — install the native phpredis extension instead. I had to add it to the base Docker image; it's the kind of thing that doesn't matter on your laptop and very much matters at a few hundred jobs a second.
Horizon is great. Just remember that every "process" on that lovely dashboard is your whole app sitting in RAM, and the defaults were written for a smaller app than yours.