skip to content

Derive a sizing rule for a worker pool from first principles: how many workers does a purely CPU-bound workload need, and how does that number change when each task spends most of its time waiting on something external?

level: middleimportance: must knowfreq 55%

answer

  1. N = C x U x (1 + W/S)
  2. runnable fraction S/(S+W) per worker
  3. W = 0 -> pool ~ core count
  4. blocking coefficient = 1/(1 - blocked fraction)
  5. downstream capacity can override the formula

basics

~20 s

A worker is runnable only the fraction S/(S+W) of its life, where S is compute time and W is wait time. To keep C cores busy at target utilization U you need N = C x U x (1 + W/S) workers. CPU-bound means W is zero, so N is about the core count.

solid answer

~60 s

Start from what a worker does: it computes for S and waits for W per task, so it wants a core only S/(S+W) of the time. To keep C cores busy at target utilization U, choose N so that the runnable fraction times the worker count equals the CPU demand: N x S/(S+W) = C x U, giving **N = C x U x (1 + W/S)**. Two readings follow. Purely CPU-bound work has W = 0, so N = C x U - the core count, perhaps plus one to cover incidental stalls. Work that waits nine times longer than it computes needs ten times the core count. The formula is a first estimate, not a law: it assumes W is genuinely non-CPU time, that all tasks in the pool have similar W/S, and above all that the resource being waited on can absorb N concurrent requests. If the real bottleneck is the downstream service or a connection pool, the formula sizes you into overloading it, and the right limit becomes whatever that dependency can sustain.

code

text · 4 lines
text
worker: [CPU 5ms][----- wait 45ms -----][CPU 5ms][----- wait 45ms -----]
runnable fraction = 5/50 = 0.1
to keep 8 cores 80% busy: N x 0.1 = 6.4  ->  N = 64 workers
equivalently N = 8 x 0.8 x (1 + 45/5) = 8 x 0.8 x 10 = 64

go deeper

for a junior

Know that compute-heavy pools sit near the core count and that waiting work needs many more workers.

for a middle

Derive N = C x U x (1 + W/S) from the runnable fraction and plug in both extremes with real numbers.

for a senior

Explain the assumptions that break it - hidden CPU inside the wait, mixed classes, downstream saturation, connection limits - and describe how you would measure S and W.

for a principal

Position the formula as a starting point inside a control loop, and treat pool size as a concurrency limit protecting a dependency rather than a throughput knob.

## The quantities For one task let **S** be service time - the time it actually needs a CPU - and **W** be wait time, the time it is blocked and consuming no CPU. Let **C** be the number of hardware threads and **U** the utilization you are targeting, a number below 1. ## The derivation A worker cycles through S of computing and W of waiting, so at any instant the probability that it wants a CPU is S/(S+W). With N independent workers the expected CPU demand is N x S/(S+W) cores. Setting demand equal to the CPU you intend to consume: ``` N x S/(S+W) = C x U N = C x U x (S+W)/S N = C x U x (1 + W/S) ``` The factor (1 + W/S) is the whole content of the rule: it is how many workers must exist for every one that is currently running. It is sometimes called the blocking coefficient, written 1/(1-b) where b is the blocked fraction W/(S+W) - the same number in different clothing. ## Reading it at the two extremes **W = 0 (compute only).** N = C x U. Sizing at the core count is the answer; a small addition is common because even compute-bound tasks stall on page faults, allocation, or an occasional log write, and a spare runnable worker keeps the core from idling during those stalls. Going far above buys nothing and costs switching and cache residency. **W >> S (mostly waiting).** A task that computes 5 ms and waits 95 ms has W/S = 19, so 8 cores at 80% utilization implies about 8 x 0.8 x 20 = 128 workers. This is why blocking I/O designs end up with pools in the hundreds while compute pools stay in single digits, and why one pool cannot serve both: a single N cannot satisfy two workloads whose W/S differ by two orders of magnitude. ## Relationship to Little's law The rule is the same statement as Little's law applied to the workers. In a stable system the average number of tasks in service equals arrival rate times average residence time, L = lambda x (S+W). Substituting the throughput a saturated CPU can sustain, lambda = C x U / S, gives N = C x U x (S+W)/S again. Recognizing that the formula is a queueing identity rather than folklore is the difference between reciting it and being able to adapt it. ## Where it breaks - **W is not always free.** Waits that involve encryption, deserialization, or busy polling contain hidden CPU time, so the true S is larger than assumed and the formula oversizes. - **Mixed W/S in one pool.** The formula's N is meaningless for a pool where some tasks wait 100 ms and others compute for 100 ms; that is an argument for separate pools, not for averaging. - **The downstream is the real constraint.** If the dependency saturates at 50 concurrent calls, sizing to 128 does not create throughput; it creates a queue on the other side, longer latency, and possibly a collapse there. The pool then acts as a concurrency limit and should be set to the dependency's healthy operating point. - **Memory and connections.** Each worker may hold a connection or a large buffer, so the sustainable N can be capped by those long before CPU or latency arguments bite. - **U is not 1.** Targeting full utilization makes queueing delay explode, so U is usually chosen in the 0.6-0.8 range for latency-sensitive work. ## How to obtain S and W Measure, do not guess: instrument the task to record total residence time and CPU time (inferring W as the difference), or compare process CPU time against wall time under a controlled load. Then use the formula for a starting value and adjust by observing utilization, queue depth and latency at each setting. The formula's real role is to give a defensible starting point and, more importantly, to tell you which direction to move when the numbers disagree.

  • The formula suggests 200 workers for an I/O-bound pool, but the downstream service degrades past 40 concurrent calls. What do you do?
    Size to the dependency, not to the formula. The formula assumes the wait represents free capacity; when the waited-on resource is itself the bottleneck, extra concurrency only lengthens its queue and can push it into overload or a timeout storm. Set the pool - or a separate concurrency limiter - near the dependency's healthy operating point, make the excess wait visible as queueing or fast rejection at your edge, and revisit only when the dependency's capacity changes.
  • Why add one or two workers beyond the core count for CPU-bound work?
    Because 'CPU-bound' is never absolute: tasks take page faults, allocate, occasionally log, or hit a brief lock. During those micro-stalls the core would idle if no other worker were runnable. One or two spares keep utilization near the target at negligible switching cost, while going substantially above adds context switching and cache pressure without adding parallelism.

saying these in an interview costs you the question

  • Quoting the formula without being able to derive or explain the (1 + W/S) factor
  • Assuming wait time consumes no CPU when it involves parsing, TLS, or polling
  • Applying one number to a pool with mixed workload classes
  • Ignoring that the downstream dependency may saturate long before your CPU does
  • Targeting 100% utilization in the formula for a latency-sensitive service

context