"How many workers should we run?" usually gets answered with a number somebody remembers from a previous project. You can do better than that in about five minutes, because the arithmetic is simple and the answer is usually not what people guess.

The only formula you need

One worker handles one job at a time. So the work a single worker can get through is:

throughput per worker = 1 / average job duration

A job averaging 500ms means two jobs a second, per worker. To keep up with incoming work:

workers needed = jobs arriving per second × average job duration

If twenty jobs arrive per second and each takes 500ms, you need ten workers just to break even. Nine and your backlog grows forever. Eleven and you have a little headroom.

That is the whole model. Everything else is a constraint on top of it.

Get the two numbers

Average duration is the one people guess wrong, usually because a few slow jobs dominate. Measure it rather than estimating, and look at the spread as well as the mean. If half your jobs take 50ms and a few take 30 seconds, one number will not describe them, and that is a signal to split them onto separate queues with their own workers.

Arrival rate is jobs dispatched per second at the times that matter, not the daily average. A daily average hides the fact that everything arrives between nine and ten in the morning.

Then check the ceilings

The formula tells you what you need. These tell you what you can have.

Memory. Each worker is a full application instance, typically somewhere between 60 and 150MB depending on what you load. Ten workers is a couple of gigabytes before your app has done anything interesting. Workers also grow over time, which is its own problem.

CPU. If jobs are CPU-bound (image work, encoding, big in-memory transforms), you cannot usefully run many more workers than you have cores. If they are I/O-bound, which most queued work is, workers spend their time waiting and you can comfortably run several per core.

Database connections. Every worker holds at least one. Thirty workers against a database allowing a hundred connections, alongside your web tier, is closer to the limit than it looks.

The thing you are calling. This is the ceiling people forget. If jobs hit an API that tolerates five concurrent requests, worker eleven does not make anything faster, it just gets rejected. At that point you want a concurrency limit, not more workers.

Measure the right thing afterwards

Worker count is an input. The output worth watching is queue wait time: how long a job sits between being dispatched and being picked up.

If wait time is near zero, you have enough workers, possibly too many. If it climbs during peaks and recovers afterwards, you are sized for the average and riding out the spikes, which is often the right call. If it climbs and never recovers, you are genuinely under-provisioned and no amount of tuning elsewhere will fix it.

Backlog depth is easy to check from the app:

Queue::size('default');

Log it on a schedule and you have a graph worth looking at.

Start low and add

Two or three workers per queue is a sensible starting point for most applications. Watch wait time, add workers while it improves, and stop when it does not, because that is the point where something else became the bottleneck.

The common mistake is starting with twenty because the box has cores to spare, then discovering the database was the constraint all along and the extra workers were just making it worse.

Wrapping up

Multiply arrival rate by average job duration to get the number you actually need, then sanity-check it against memory, CPU, database connections, and whatever the jobs are calling. Watch queue wait time rather than worker count, and remember more workers only help while your workers are the bottleneck.

Related