Skip to content
ansezz.

▸ Free tool

Server Capacity Calculator.

How many workers do you actually need? Enter throughput, latency, and concurrency per worker — Little's Law sizes the fleet, and the queueing table shows what the last 20% of utilization costs you.

▸ The law is exact, your inputs aren't

L = λ × W holds for any stable system. Everything downstream of it is only as honest as the latency and concurrency you feed in.

Solve for

▸ Same law, both directions. Pick the one you don't know yet.

▸ Peak, not daily average. Divide the busiest hour by 3,600.

▸ Server-side handler time, measured at low load. Not the number your users see during a spike.

▸ How many requests one worker holds at once. Sync PHP/Python workers: 1.

▸ 60-75% for user-facing traffic. 85-90% only for queue consumers.

Workers needed

In flight (L = λ × W)
Slots in the pool
Workers at 100%
Real utilization
Headroom before saturation
Throughput per worker

Runs in your browser · nothing uploaded

Runtime starting points.

Default concurrency and a plausible latency for four common runtimes
Runtime Concurrency Latency Apply
Laravel Octane worker Swoole / FrankenPHP, one request per worker process 1 / worker 40 ms
Node single process Event loop, I/O-bound handlers, nothing CPU-heavy 40 / worker 120 ms
Python sync WSGI worker Gunicorn sync worker — blocks for the whole request 1 / worker 80 ms
Go goroutine service One goroutine per request, bounded by CPU and pool limits 200 / worker 25 ms

▸ Starting points, not truths. Every one of these moves with your middleware, your database, and your GC settings — measure yours.

What the last 20% costs.

Expected queue wait and total response time at rising utilization
Utilization Queue wait Total response Wait ÷ service time
50%
70%
80%
90%
95%
99%

How this is computed. Exact Erlang C for the M/M/c model: Wq / S = C(c, a) ÷ (c × (1 − ρ)), where a = ρ × c is offered load in erlangs and C comes from the standard Erlang B recursion B(n) = a·B(n−1) ÷ (n + a·B(n−1)). The arithmetic is exact. The model is not your traffic. M/M/c assumes Poisson arrivals, exponentially distributed service times, one shared queue, infinitely patient clients, and zero retries. It also treats every concurrency slot as an independent server, which flatters an I/O-bound runtime whose forty slots share one CPU. Real bursts are worse than Poisson, real latency has a fatter tail, and a retry storm breaks the model outright — so read these numbers as the optimistic floor on queueing pain, not a forecast. What they are good for is the shape: the ratio between the 70% row and the 95% row is the point.

Little's Law, stated plainly

L = λ × W. The average number of requests inside your service equals the rate they arrive at, multiplied by how long each one stays. Nothing else. If 120 requests per second arrive and each takes 200 ms, then on average 24 requests are inside your service at any instant — 120 × 0.2. Those 24 have to live somewhere: in a worker slot, in a goroutine, on an event loop, or in a queue in front of all of them. That is the whole sizing problem, and it is the number the tool prints as L.

The remarkable thing about this law is that it assumes nothing. No distribution of arrivals, no distribution of service times, no queueing discipline, no independence. Any stable system, over a long enough window, obeys it. That makes it the one piece of capacity maths you can trust completely — and also the reason it can never tell you about your worst minute, because averages have no tails.

The tool applies it in both directions. Workers needed computes L from your throughput and latency, divides by concurrency per worker, then divides again by target utilization and rounds up. Max throughput runs it backwards: your worker count times concurrency gives total slots, slots times target utilization gives busy slots, and busy slots divided by latency gives the arrival rate that keeps you there. Same equation, rearranged.

Why 100% utilization is a trap

The naive answer to "how many workers" is L divided by concurrency. That number is correct and useless, because it sizes for a world where requests arrive on a metronome. They don't. Arrivals cluster, and when two requests want the last free slot at the same time, one of them waits.

Queueing delay scales with 1 / (1 − ρ). At 50% utilization the term is 2. At 90% it is 10. At 99% it is 100. That is why the table above bends the way it does: the first 70% of capacity is nearly free, and the last 5% costs more than everything before it combined. Utilization is not an efficiency score. Past about 80% it is a latency dial you are turning the wrong way.

There is one genuine mitigation, and the table shows it: pool size. A single worker with one slot at 90% utilization queues for nine service times. Forty slots behind one queue at the same 90% barely queue at all, because a free slot is almost always available somewhere. This is the entire argument for a shared queue in front of a worker pool instead of per-worker queues, and it is why a load balancer doing least-outstanding-requests beats round-robin under load.

p99, not average, decides the fleet

Little's Law wants the mean, so the mean is the right input for the steady-state number. It is the wrong input for surviving a burst. A service with 80 ms average latency and 900 ms p99 does not occupy workers for 80 ms — it occupies them for 80 ms most of the time and 900 ms exactly when everyone shows up at once. During those seconds the effective W is an order of magnitude larger, L jumps with it, and a fleet sized on the average has no slots left.

Practical approach: run the calculator once with your average to get the baseline, once with your p99 to get the burst budget, and put the fleet somewhere between them depending on how expensive a queued request is. If a slow request holds a database connection or a file handle, size closer to p99 — the resource, not the CPU, is what runs out. And if you do not know your p99, that is the first thing to fix; a mean latency with no percentile behind it is a number you cannot plan on.

Concurrency is not parallelism

The "concurrency per worker" field is the one people get wrong, because it means different things depending on what your requests spend their time doing.

  I/O-bound CPU-bound
Where time goes Waiting on DB, HTTP, disk, an LLM Hashing, parsing, rendering, inference
Concurrency per worker High — dozens to hundreds of waiting requests ≈ cores available to that worker, and no more
What adding slots does Absorbs more waiting, latency barely moves Nothing but queue inside the process
Scale by Raising concurrency, then adding workers Adding workers or cores — full stop
Failure signature Connection pool exhausted downstream Run queue climbs, every response slows at once

Concurrency is how many requests are in progress; parallelism is how many are actually executing on a core right now. A Node process can hold four hundred concurrent requests and still be running exactly one line of your code at a time. That is fine while they are all waiting on Postgres, and catastrophic the moment one of them starts parsing a 10 MB JSON payload — the event loop stalls and all four hundred stall with it. Set concurrency to what the runtime can hold, then check whether your handlers actually spend their time waiting. If they don't, the honest number is the core count.

Autoscaling doesn't remove the headroom

The most common reason a correctly-sized service falls over is that the replacement capacity arrives late. Metrics scrape on an interval, the autoscaler evaluates on an interval, a new pod schedules, an image pulls, the runtime boots, the JIT warms, connection pools fill, and the load balancer's health check has to pass twice. Thirty seconds is a good day on Kubernetes with a warm image cache; two to four minutes is normal on a cold VM.

For that entire window your existing workers absorb the whole surge — which is precisely what the target utilization field is buying you. Rough rule: your headroom should cover the traffic growth you can see during one full scale-out cycle. A service that doubles in 60 seconds and takes 90 seconds to add a pod needs to be sitting near 50%, not 70%, or it needs a pre-warmed floor of replicas. Scaling on CPU makes this worse for I/O-bound services, since CPU stays flat while the request queue grows — scale on in-flight requests or queue depth instead, which is the L this tool just computed for you.

And do not size for the peak you have today. Size for the peak plus one failure domain: if a third of your pods live in an availability zone that can disappear, the remaining two thirds have to run the whole load at an acceptable utilization. That is a multiplier on the number above, not a rounding error.

Questions people ask.

How many servers do I need for X requests per second?

Multiply your target requests per second by the average time one request occupies a worker, in seconds. That product is the number of requests in flight at any instant — Little's Law. Divide it by how many requests a single worker handles concurrently, then divide again by your target utilization. At 300 req/s, 120 ms of service time, 40 concurrent slots per worker and a 70% target, that is 300 × 0.12 = 36 in flight, 36 ÷ 40 ÷ 0.7 = 1.3, so 2 workers.

What is Little's Law in simple terms?

L = λ × W. The average number of items inside a system equals how fast they arrive multiplied by how long each one stays. It holds for any stable system regardless of arrival pattern, service time distribution, or queueing discipline — it is arithmetic, not a model. The catch is that it describes long-run averages, so it tells you the floor on capacity and nothing at all about your worst minute.

What utilization should I target for a web service?

60-75% for anything user-facing. Queueing delay is proportional to 1/(1-ρ), so the cost of the last few percent is brutal: going from 70% to 90% roughly triples queue wait on a single-slot worker, and 90% to 95% doubles it again. Batch workers and queue consumers can run at 85-90% because nobody is watching a spinner. Anything above 90% on a synchronous path means your p99 is set by queueing, not by your code.

Should I size workers with average latency or p99?

Size the steady state with average latency, then sanity-check the fleet with p99. Little's Law uses the mean by definition, so the average is the mathematically correct input for the long-run number. But traffic arrives in bursts, and during a burst it is the slow requests that pin workers down — if your average is 80 ms and your p99 is 900 ms, a burst of slow requests occupies eleven times more slots than the average predicts. Run the calculator twice and treat the p99 answer as your burst budget.

How many Gunicorn or Octane workers do I need?

Both default to one request per worker process, so concurrency per worker is 1 and the answer is simply in-flight requests divided by your target utilization, rounded up. At 50 req/s with 80 ms handlers that is 4 in flight, or 6 workers at 70%. Then cap it by memory and CPU: a process that needs 250 MB resident cannot be scaled to 40 copies on a 4 GB box no matter what the arithmetic says.

Does this capacity calculator send my numbers anywhere?

No. There is no backend, no analytics call on the inputs, and no network request of any kind after the page loads. Every number — Little's Law, the worker counts, and the Erlang C queueing table — is computed in JavaScript inside your tab. Switch the network off and it keeps working.

Keep reading.