Hirestack
Hirestack
Chapter 7

Capacity planning & queueing theory

📖 8 min read · 8 sections · May 2026

Why this matters

Most engineers cannot size systems. They guess. They over-provision (waste money) or under-provision (cause outages). Staff engineers reflexively quantify: throughput, latency, headroom, cost.

The underlying math — queueing theory — was developed in the 1900s for telephone networks. It applies directly to modern systems: web servers, databases, queues, CPUs. Internalising even a small subset of it gives you a quantitative model that beats intuition.

This topic is also where you understand why systems behave the way they do under load. Why does latency explode nonlinearly when utilisation hits 80%? Why does adding more workers sometimes make things worse? Why does p99 latency get much worse than median? The answers are in the math.

Little's Law — the most useful formula

Little's Law states:

L = λ × W

where:
  L = average number of items in the system
  λ = average arrival rate (items per unit time)
  W = average time spent in the system (per item)

The remarkable thing about Little's Law is its generality: it holds for any system in steady state, regardless of the distribution of arrival times or service times. As long as the system is stable (not growing infinitely), this relationship holds.

Applied examples

Web server: a service processes 1,000 requests/second (λ). Each request takes on average 50ms (W = 0.05s). How many requests are "in flight" at any moment?

L = 1,000 × 0.05 = 50 requests in flight on average

This tells you: you need a connection pool / worker pool of at least 50, and probably 2-3× that for buffer.

Database connection pool: you have an app server doing 500 queries/sec (λ), each query takes 10ms (W = 0.01s). Average concurrent queries:

L = 500 × 0.01 = 5 concurrent queries

You need at least 5 connections. With burst, maybe 20.

Kafka partition: a partition is read at 100 msg/sec by a consumer that takes 50ms per message. Concurrent messages "in flight" per partition:

L = 100 × 0.05 = 5

If your consumer can only process one message at a time (single-threaded per partition), and processing takes 50ms, your max throughput is 1 / 0.05 = 20 msg/sec. To hit 100 msg/sec, you need 5 partitions (or parallel consumers per partition).

Inverting Little's Law

Often the question is: given my system's constraint (L), how much can I scale?

λ = L / W

If my pool size is 100 (L=100 max) and each request takes 50ms (W=0.05s):
λ = 100 / 0.05 = 2,000 requests/sec max throughput

Or: if I want to hit 5,000 req/sec with 50ms latency, I need a pool of at least:

L = 5,000 × 0.05 = 250

Queueing theory — M/M/1 model

The M/M/1 queue is the simplest model: a single server (1), Markov (memoryless, Poisson) arrivals (M), Markov service times (M). Real systems are more complex but the math gives you intuition.

Parameters:

The killer result: average time in system (waiting + service) is:

W = 1 / (μ − λ) = 1 / μ × 1 / (1 − ρ)

The 1 / (1 − ρ) factor is the punchline. As ρ approaches 1 (server fully utilised), W goes to infinity. This is the utilisation curve — the most important graph in system design.

The utilisation curve, with numbers

xychart-beta
    title "Queue wait time vs utilisation (M/M/1)"
    x-axis "Utilisation %" [10, 30, 50, 60, 70, 80, 85, 90, 95, 99]
    y-axis "Wait time multiplier" 0 --> 100
    line [0.1, 0.4, 1.0, 1.5, 2.3, 4, 5.7, 9, 19, 99]

Suppose your server can process requests in 100ms (μ = 10 req/sec). What does latency look like at different load levels?

λ (load)ρ (utilisation)W (avg time in system)
1 req/sec10%111 ms
5 req/sec50%200 ms
8 req/sec80%500 ms
9 req/sec90%1,000 ms
9.5 req/sec95%2,000 ms
9.9 req/sec99%10,000 ms
10 req/sec100%

At 50% utilisation, you're 2× the unloaded latency. At 80%, you're 5×. At 95%, you're 20×. At 99%, you're 100×.

This is the most important lesson in capacity planning. Most engineers think "I'm at 80% utilisation, I have 20% headroom." Wrong. At 80% utilisation, you're already paying 5× latency cost. Target average utilisation of 50-60% for latency-sensitive systems, even though it feels wasteful. The 40% "unused" capacity is what keeps p99 reasonable.

(Note: this is the M/M/1 model. Real systems with multiple servers per queue, batching, or different service-time distributions behave differently — but the qualitative shape is similar. Many-server queues are more forgiving but the same hockey-stick exists.)

Head-of-line blocking

When work units must be processed in order, one slow unit blocks everything behind it. The single-server queue is the simplest example.

Examples in real systems

Mitigations

Tail latency amplification

Suppose each backend server has 1% probability of being slow (above some threshold). You call one backend → 1% chance of slow response.

But suppose your request fans out to 100 backends, all of which must respond before you can reply (e.g., a federated search). What's the probability that at least one is slow?

P(at least one slow) = 1 − (0.99)^100 ≈ 63%

So 63% of fanout requests will see at least one slow backend, even though only 1% of individual backend calls are slow. Tail latency amplifies with fanout.

This is the central insight in Jeff Dean and Luiz Barroso's The Tail at Scale paper (2013). Required reading.

Mitigations

Capacity-planning calculation walkthrough

You're designing an API service. Requirements:

Step 1: Concurrency. By Little's Law, average concurrent requests = 10,000 × 0.08 = 800. Plus burst: target 1,500 concurrent capacity.

Step 2: CPU. 30ms compute per request × 10,000 req/s = 300 CPU-seconds per second. So 300 cores running at 100% utilisation — but you target 50% utilisation, so 600 cores. Round up to 700.

Step 3: Network. Request: 5KB × 10,000 = 50 MB/s ingress. Response: 50KB × 10,000 = 500 MB/s egress. 500 MB/s = 4 Gbps. Need 10 Gbps NIC capacity, with headroom.

Step 4: Memory. 1,500 concurrent × ~200KB working memory per request = 300 MB. Negligible.

Step 5: Instances. If each instance is 8 CPU / 16GB / 1 Gbps:

Step 6: Headroom for AZ failure. Spread across 3 AZs; design for losing one. Need 88 × 1.5 = 132 instances total.

This is the kind of calculation Staff engineers do reflexively. The numbers might be approximations, but the discipline of going through each resource explicitly prevents "let's just put it on a couple of c5.xlarge and see."

Applied scenarios

Scenario: Your service runs at 90% CPU utilisation. Latency is fine. A teammate says "we have headroom." Do you agree?
No. At 90% utilisation on a single-threaded resource, you're at ~10× unloaded latency by M/M/1. The "headroom" is illusory — small traffic spikes will cause latency cliffs. If multi-threaded, you have more buffer but the same trend applies. For latency-sensitive services, target 50-60% average utilisation. The extra cost is paying for predictable tail latency.
Scenario: Your service makes calls to 20 backends in parallel and waits for all of them. Each backend has p99 of 100ms. What's your p99?
If backends are independent, P(all backends finish under 100ms) = (0.99)^20 ≈ 82%. So 18% of your requests see at least one backend at 100ms+. Your p99 latency is no longer 100ms — it's whatever the p99 of "max of 20 latencies" is, which is roughly the p99.95 of an individual backend. If individual backends p99 = 100ms and p99.95 = 300ms, your overall p99 ≈ 300ms.
Scenario: A team wants to deploy a service with 1 worker per instance and "see how it scales." What questions do you ask?
(1) What's the expected request rate and per-request processing time? Compute Little's Law to find required concurrency. (2) What's the latency target? Pick utilisation target to match. (3) What's the failure mode when load exceeds capacity? Queue grows? Drops requests? OOMs? (4) What's the resource bottleneck — CPU, memory, network, downstream? Different bottlenecks need different scaling strategies. (5) What's the auto-scaling trigger? CPU at 70%? Request rate? Queue depth?

Further reading

Capacity planning & queueing theory